synabun 2.0.7 → 2.0.9

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 (346) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +1 -1
  3. package/hooks/claude-code/post-plan.mjs +6 -30
  4. package/hooks/claude-code/session-start.mjs +1 -4
  5. package/hooks/claude-code/shared.mjs +106 -72
  6. package/lib/package-processes.js +670 -0
  7. package/lib/runtime-dependencies.js +225 -0
  8. package/mcp-server/dist/services/browser-bench.d.ts +1 -1
  9. package/mcp-server/dist/services/browser-bench.d.ts.map +1 -1
  10. package/mcp-server/dist/services/browser-bench.js +1 -1
  11. package/mcp-server/dist/services/browser-bench.js.map +1 -1
  12. package/mcp-server/dist/services/categories.d.ts +35 -2
  13. package/mcp-server/dist/services/categories.d.ts.map +1 -1
  14. package/mcp-server/dist/services/categories.js +77 -33
  15. package/mcp-server/dist/services/categories.js.map +1 -1
  16. package/mcp-server/dist/services/desktop-bench.d.ts +7 -7
  17. package/mcp-server/dist/services/desktop-bench.d.ts.map +1 -1
  18. package/mcp-server/dist/services/desktop-bench.js +1 -1
  19. package/mcp-server/dist/services/desktop-bench.js.map +1 -1
  20. package/mcp-server/dist/tools/agents.d.ts +1 -1
  21. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  22. package/mcp-server/dist/tools/agents.js +1 -1
  23. package/mcp-server/dist/tools/agents.js.map +1 -1
  24. package/mcp-server/dist/tools/bluesky.js +1 -1
  25. package/mcp-server/dist/tools/bluesky.js.map +1 -1
  26. package/mcp-server/dist/tools/browser-advanced.d.ts +1 -1
  27. package/mcp-server/dist/tools/browser-advanced.d.ts.map +1 -1
  28. package/mcp-server/dist/tools/browser-advanced.js +1 -1
  29. package/mcp-server/dist/tools/browser-advanced.js.map +1 -1
  30. package/mcp-server/dist/tools/browser-batch.d.ts +1 -1
  31. package/mcp-server/dist/tools/browser-batch.d.ts.map +1 -1
  32. package/mcp-server/dist/tools/browser-batch.js +1 -1
  33. package/mcp-server/dist/tools/browser-batch.js.map +1 -1
  34. package/mcp-server/dist/tools/browser-cheatsheet.d.ts +1 -1
  35. package/mcp-server/dist/tools/browser-cheatsheet.d.ts.map +1 -1
  36. package/mcp-server/dist/tools/browser-cheatsheet.js +1 -1
  37. package/mcp-server/dist/tools/browser-cheatsheet.js.map +1 -1
  38. package/mcp-server/dist/tools/browser-interact.d.ts +1 -1
  39. package/mcp-server/dist/tools/browser-interact.d.ts.map +1 -1
  40. package/mcp-server/dist/tools/browser-interact.js +1 -1
  41. package/mcp-server/dist/tools/browser-interact.js.map +1 -1
  42. package/mcp-server/dist/tools/browser-navigate.d.ts +1 -1
  43. package/mcp-server/dist/tools/browser-navigate.d.ts.map +1 -1
  44. package/mcp-server/dist/tools/browser-navigate.js +1 -1
  45. package/mcp-server/dist/tools/browser-navigate.js.map +1 -1
  46. package/mcp-server/dist/tools/browser-observe.d.ts +1 -1
  47. package/mcp-server/dist/tools/browser-observe.d.ts.map +1 -1
  48. package/mcp-server/dist/tools/browser-observe.js +1 -1
  49. package/mcp-server/dist/tools/browser-observe.js.map +1 -1
  50. package/mcp-server/dist/tools/card-tools.d.ts +1 -1
  51. package/mcp-server/dist/tools/card-tools.d.ts.map +1 -1
  52. package/mcp-server/dist/tools/card-tools.js +1 -1
  53. package/mcp-server/dist/tools/card-tools.js.map +1 -1
  54. package/mcp-server/dist/tools/category.d.ts +1 -1
  55. package/mcp-server/dist/tools/category.d.ts.map +1 -1
  56. package/mcp-server/dist/tools/category.js +5 -1
  57. package/mcp-server/dist/tools/category.js.map +1 -1
  58. package/mcp-server/dist/tools/choice.d.ts +1 -1
  59. package/mcp-server/dist/tools/choice.d.ts.map +1 -1
  60. package/mcp-server/dist/tools/choice.js +1 -1
  61. package/mcp-server/dist/tools/choice.js.map +1 -1
  62. package/mcp-server/dist/tools/computer.d.ts +1 -1
  63. package/mcp-server/dist/tools/computer.d.ts.map +1 -1
  64. package/mcp-server/dist/tools/computer.js +1 -1
  65. package/mcp-server/dist/tools/computer.js.map +1 -1
  66. package/mcp-server/dist/tools/discord-channel.d.ts +1 -1
  67. package/mcp-server/dist/tools/discord-channel.d.ts.map +1 -1
  68. package/mcp-server/dist/tools/discord-channel.js +1 -1
  69. package/mcp-server/dist/tools/discord-channel.js.map +1 -1
  70. package/mcp-server/dist/tools/discord-guild.d.ts +1 -1
  71. package/mcp-server/dist/tools/discord-guild.d.ts.map +1 -1
  72. package/mcp-server/dist/tools/discord-guild.js +1 -1
  73. package/mcp-server/dist/tools/discord-guild.js.map +1 -1
  74. package/mcp-server/dist/tools/discord-member.d.ts +1 -1
  75. package/mcp-server/dist/tools/discord-member.d.ts.map +1 -1
  76. package/mcp-server/dist/tools/discord-member.js +1 -1
  77. package/mcp-server/dist/tools/discord-member.js.map +1 -1
  78. package/mcp-server/dist/tools/discord-message.d.ts +1 -1
  79. package/mcp-server/dist/tools/discord-message.d.ts.map +1 -1
  80. package/mcp-server/dist/tools/discord-message.js +1 -1
  81. package/mcp-server/dist/tools/discord-message.js.map +1 -1
  82. package/mcp-server/dist/tools/discord-onboarding.d.ts +1 -1
  83. package/mcp-server/dist/tools/discord-onboarding.d.ts.map +1 -1
  84. package/mcp-server/dist/tools/discord-onboarding.js +1 -1
  85. package/mcp-server/dist/tools/discord-onboarding.js.map +1 -1
  86. package/mcp-server/dist/tools/discord-role.d.ts +1 -1
  87. package/mcp-server/dist/tools/discord-role.d.ts.map +1 -1
  88. package/mcp-server/dist/tools/discord-role.js +1 -1
  89. package/mcp-server/dist/tools/discord-role.js.map +1 -1
  90. package/mcp-server/dist/tools/discord-thread.d.ts +1 -1
  91. package/mcp-server/dist/tools/discord-thread.d.ts.map +1 -1
  92. package/mcp-server/dist/tools/discord-thread.js +1 -1
  93. package/mcp-server/dist/tools/discord-thread.js.map +1 -1
  94. package/mcp-server/dist/tools/discord-webhook.d.ts +1 -1
  95. package/mcp-server/dist/tools/discord-webhook.d.ts.map +1 -1
  96. package/mcp-server/dist/tools/discord-webhook.js +1 -1
  97. package/mcp-server/dist/tools/discord-webhook.js.map +1 -1
  98. package/mcp-server/dist/tools/fb-groups.d.ts +1 -1
  99. package/mcp-server/dist/tools/fb-groups.d.ts.map +1 -1
  100. package/mcp-server/dist/tools/fb-groups.js +1 -1
  101. package/mcp-server/dist/tools/fb-groups.js.map +1 -1
  102. package/mcp-server/dist/tools/forget.d.ts +1 -1
  103. package/mcp-server/dist/tools/forget.d.ts.map +1 -1
  104. package/mcp-server/dist/tools/forget.js +1 -1
  105. package/mcp-server/dist/tools/forget.js.map +1 -1
  106. package/mcp-server/dist/tools/git-tools.d.ts +1 -1
  107. package/mcp-server/dist/tools/git-tools.d.ts.map +1 -1
  108. package/mcp-server/dist/tools/git-tools.js +1 -1
  109. package/mcp-server/dist/tools/git-tools.js.map +1 -1
  110. package/mcp-server/dist/tools/gsc/experience.d.ts +1 -1
  111. package/mcp-server/dist/tools/gsc/experience.d.ts.map +1 -1
  112. package/mcp-server/dist/tools/gsc/experience.js +1 -1
  113. package/mcp-server/dist/tools/gsc/experience.js.map +1 -1
  114. package/mcp-server/dist/tools/gsc/indexing.d.ts +1 -1
  115. package/mcp-server/dist/tools/gsc/indexing.d.ts.map +1 -1
  116. package/mcp-server/dist/tools/gsc/indexing.js +1 -1
  117. package/mcp-server/dist/tools/gsc/indexing.js.map +1 -1
  118. package/mcp-server/dist/tools/gsc/inspect.d.ts +1 -1
  119. package/mcp-server/dist/tools/gsc/inspect.d.ts.map +1 -1
  120. package/mcp-server/dist/tools/gsc/inspect.js +1 -1
  121. package/mcp-server/dist/tools/gsc/inspect.js.map +1 -1
  122. package/mcp-server/dist/tools/gsc/navigate.d.ts +1 -1
  123. package/mcp-server/dist/tools/gsc/navigate.d.ts.map +1 -1
  124. package/mcp-server/dist/tools/gsc/navigate.js +1 -1
  125. package/mcp-server/dist/tools/gsc/navigate.js.map +1 -1
  126. package/mcp-server/dist/tools/gsc/performance.d.ts +1 -1
  127. package/mcp-server/dist/tools/gsc/performance.d.ts.map +1 -1
  128. package/mcp-server/dist/tools/gsc/performance.js +1 -1
  129. package/mcp-server/dist/tools/gsc/performance.js.map +1 -1
  130. package/mcp-server/dist/tools/gsc/settings.d.ts +1 -1
  131. package/mcp-server/dist/tools/gsc/settings.d.ts.map +1 -1
  132. package/mcp-server/dist/tools/gsc/settings.js +1 -1
  133. package/mcp-server/dist/tools/gsc/settings.js.map +1 -1
  134. package/mcp-server/dist/tools/image-tools.d.ts +1 -1
  135. package/mcp-server/dist/tools/image-tools.d.ts.map +1 -1
  136. package/mcp-server/dist/tools/image-tools.js +1 -1
  137. package/mcp-server/dist/tools/image-tools.js.map +1 -1
  138. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +1 -1
  139. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +1 -1
  140. package/mcp-server/dist/tools/leonardo-browser-tools.js +1 -1
  141. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +1 -1
  142. package/mcp-server/dist/tools/loop.d.ts +1 -1
  143. package/mcp-server/dist/tools/loop.d.ts.map +1 -1
  144. package/mcp-server/dist/tools/loop.js +1 -1
  145. package/mcp-server/dist/tools/loop.js.map +1 -1
  146. package/mcp-server/dist/tools/memories.d.ts +1 -1
  147. package/mcp-server/dist/tools/memories.d.ts.map +1 -1
  148. package/mcp-server/dist/tools/memories.js +1 -1
  149. package/mcp-server/dist/tools/memories.js.map +1 -1
  150. package/mcp-server/dist/tools/morelogin.d.ts +1 -1
  151. package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
  152. package/mcp-server/dist/tools/morelogin.js +1 -1
  153. package/mcp-server/dist/tools/morelogin.js.map +1 -1
  154. package/mcp-server/dist/tools/profile.d.ts +1 -1
  155. package/mcp-server/dist/tools/profile.d.ts.map +1 -1
  156. package/mcp-server/dist/tools/profile.js +1 -1
  157. package/mcp-server/dist/tools/profile.js.map +1 -1
  158. package/mcp-server/dist/tools/recall.d.ts +1 -1
  159. package/mcp-server/dist/tools/recall.d.ts.map +1 -1
  160. package/mcp-server/dist/tools/recall.js +1 -1
  161. package/mcp-server/dist/tools/recall.js.map +1 -1
  162. package/mcp-server/dist/tools/reflect.d.ts +1 -1
  163. package/mcp-server/dist/tools/reflect.d.ts.map +1 -1
  164. package/mcp-server/dist/tools/reflect.js +1 -1
  165. package/mcp-server/dist/tools/reflect.js.map +1 -1
  166. package/mcp-server/dist/tools/remember.d.ts +1 -1
  167. package/mcp-server/dist/tools/remember.d.ts.map +1 -1
  168. package/mcp-server/dist/tools/remember.js +13 -11
  169. package/mcp-server/dist/tools/remember.js.map +1 -1
  170. package/mcp-server/dist/tools/restore.d.ts +1 -1
  171. package/mcp-server/dist/tools/restore.d.ts.map +1 -1
  172. package/mcp-server/dist/tools/restore.js +1 -1
  173. package/mcp-server/dist/tools/restore.js.map +1 -1
  174. package/mcp-server/dist/tools/style-guide-tools.d.ts +1 -1
  175. package/mcp-server/dist/tools/style-guide-tools.d.ts.map +1 -1
  176. package/mcp-server/dist/tools/style-guide-tools.js +1 -1
  177. package/mcp-server/dist/tools/style-guide-tools.js.map +1 -1
  178. package/mcp-server/dist/tools/sync.d.ts +1 -1
  179. package/mcp-server/dist/tools/sync.d.ts.map +1 -1
  180. package/mcp-server/dist/tools/sync.js +1 -1
  181. package/mcp-server/dist/tools/sync.js.map +1 -1
  182. package/mcp-server/dist/tools/tictactoe-tools.d.ts +1 -1
  183. package/mcp-server/dist/tools/tictactoe-tools.d.ts.map +1 -1
  184. package/mcp-server/dist/tools/tictactoe-tools.js +1 -1
  185. package/mcp-server/dist/tools/tictactoe-tools.js.map +1 -1
  186. package/mcp-server/dist/tools/utils.d.ts +1 -1
  187. package/mcp-server/dist/tools/utils.d.ts.map +1 -1
  188. package/mcp-server/dist/tools/utils.js +1 -1
  189. package/mcp-server/dist/tools/utils.js.map +1 -1
  190. package/mcp-server/dist/tools/whatsapp-send-file.d.ts +1 -1
  191. package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -1
  192. package/mcp-server/dist/tools/whatsapp-send-file.js +1 -1
  193. package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -1
  194. package/mcp-server/dist/tools/whiteboard-tools.d.ts +3 -3
  195. package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
  196. package/mcp-server/dist/tools/whiteboard-tools.js +1 -1
  197. package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
  198. package/mcp-server/dist/tools/youtube/analytics.d.ts +1 -1
  199. package/mcp-server/dist/tools/youtube/analytics.d.ts.map +1 -1
  200. package/mcp-server/dist/tools/youtube/analytics.js +1 -1
  201. package/mcp-server/dist/tools/youtube/analytics.js.map +1 -1
  202. package/mcp-server/dist/tools/youtube/download.d.ts +1 -1
  203. package/mcp-server/dist/tools/youtube/download.d.ts.map +1 -1
  204. package/mcp-server/dist/tools/youtube/download.js +1 -1
  205. package/mcp-server/dist/tools/youtube/download.js.map +1 -1
  206. package/mcp-server/dist/tools/youtube/media.d.ts +1 -1
  207. package/mcp-server/dist/tools/youtube/media.d.ts.map +1 -1
  208. package/mcp-server/dist/tools/youtube/media.js +1 -1
  209. package/mcp-server/dist/tools/youtube/media.js.map +1 -1
  210. package/mcp-server/dist/tools/youtube/metadata.d.ts +1 -1
  211. package/mcp-server/dist/tools/youtube/metadata.d.ts.map +1 -1
  212. package/mcp-server/dist/tools/youtube/metadata.js +1 -1
  213. package/mcp-server/dist/tools/youtube/metadata.js.map +1 -1
  214. package/mcp-server/dist/tools/youtube/navigate.d.ts +1 -1
  215. package/mcp-server/dist/tools/youtube/navigate.d.ts.map +1 -1
  216. package/mcp-server/dist/tools/youtube/navigate.js +1 -1
  217. package/mcp-server/dist/tools/youtube/navigate.js.map +1 -1
  218. package/mcp-server/dist/tools/youtube/playlist.d.ts +1 -1
  219. package/mcp-server/dist/tools/youtube/playlist.d.ts.map +1 -1
  220. package/mcp-server/dist/tools/youtube/playlist.js +1 -1
  221. package/mcp-server/dist/tools/youtube/playlist.js.map +1 -1
  222. package/mcp-server/dist/tools/youtube/schedule.d.ts +1 -1
  223. package/mcp-server/dist/tools/youtube/schedule.d.ts.map +1 -1
  224. package/mcp-server/dist/tools/youtube/schedule.js +1 -1
  225. package/mcp-server/dist/tools/youtube/schedule.js.map +1 -1
  226. package/mcp-server/dist/tools/youtube/source.d.ts +1 -1
  227. package/mcp-server/dist/tools/youtube/source.d.ts.map +1 -1
  228. package/mcp-server/dist/tools/youtube/source.js +1 -1
  229. package/mcp-server/dist/tools/youtube/source.js.map +1 -1
  230. package/mcp-server/dist/tools/youtube/upload.d.ts +1 -1
  231. package/mcp-server/dist/tools/youtube/upload.d.ts.map +1 -1
  232. package/mcp-server/dist/tools/youtube/upload.js +4 -9
  233. package/mcp-server/dist/tools/youtube/upload.js.map +1 -1
  234. package/mcp-server/dist/tui.js +16 -4
  235. package/mcp-server/dist/tui.js.map +1 -1
  236. package/mcp-server/package-lock.json +3 -3
  237. package/mcp-server/package.json +3 -3
  238. package/mcp-server/src/services/browser-bench.ts +1 -1
  239. package/mcp-server/src/services/categories.ts +77 -32
  240. package/mcp-server/src/services/desktop-bench.ts +1 -1
  241. package/mcp-server/src/tools/agents.ts +1 -1
  242. package/mcp-server/src/tools/bluesky.ts +1 -1
  243. package/mcp-server/src/tools/browser-advanced.ts +1 -1
  244. package/mcp-server/src/tools/browser-batch.ts +1 -1
  245. package/mcp-server/src/tools/browser-cheatsheet.ts +1 -1
  246. package/mcp-server/src/tools/browser-interact.ts +1 -1
  247. package/mcp-server/src/tools/browser-navigate.ts +1 -1
  248. package/mcp-server/src/tools/browser-observe.ts +1 -1
  249. package/mcp-server/src/tools/card-tools.ts +1 -1
  250. package/mcp-server/src/tools/category.ts +6 -1
  251. package/mcp-server/src/tools/choice.ts +1 -1
  252. package/mcp-server/src/tools/computer.ts +1 -1
  253. package/mcp-server/src/tools/discord-channel.ts +1 -1
  254. package/mcp-server/src/tools/discord-guild.ts +1 -1
  255. package/mcp-server/src/tools/discord-member.ts +1 -1
  256. package/mcp-server/src/tools/discord-message.ts +1 -1
  257. package/mcp-server/src/tools/discord-onboarding.ts +1 -1
  258. package/mcp-server/src/tools/discord-role.ts +1 -1
  259. package/mcp-server/src/tools/discord-thread.ts +1 -1
  260. package/mcp-server/src/tools/discord-webhook.ts +1 -1
  261. package/mcp-server/src/tools/fb-groups.ts +1 -1
  262. package/mcp-server/src/tools/forget.ts +1 -1
  263. package/mcp-server/src/tools/git-tools.ts +1 -1
  264. package/mcp-server/src/tools/gsc/experience.ts +1 -1
  265. package/mcp-server/src/tools/gsc/indexing.ts +1 -1
  266. package/mcp-server/src/tools/gsc/inspect.ts +1 -1
  267. package/mcp-server/src/tools/gsc/navigate.ts +1 -1
  268. package/mcp-server/src/tools/gsc/performance.ts +1 -1
  269. package/mcp-server/src/tools/gsc/settings.ts +1 -1
  270. package/mcp-server/src/tools/image-tools.ts +1 -1
  271. package/mcp-server/src/tools/leonardo-browser-tools.ts +1 -1
  272. package/mcp-server/src/tools/loop.ts +1 -1
  273. package/mcp-server/src/tools/memories.ts +1 -1
  274. package/mcp-server/src/tools/morelogin.ts +1 -1
  275. package/mcp-server/src/tools/profile.ts +1 -1
  276. package/mcp-server/src/tools/recall.ts +1 -1
  277. package/mcp-server/src/tools/reflect.ts +1 -1
  278. package/mcp-server/src/tools/remember.ts +13 -12
  279. package/mcp-server/src/tools/restore.ts +1 -1
  280. package/mcp-server/src/tools/style-guide-tools.ts +1 -1
  281. package/mcp-server/src/tools/sync.ts +1 -1
  282. package/mcp-server/src/tools/tictactoe-tools.ts +1 -1
  283. package/mcp-server/src/tools/utils.ts +1 -1
  284. package/mcp-server/src/tools/whatsapp-send-file.ts +1 -1
  285. package/mcp-server/src/tools/whiteboard-tools.ts +1 -1
  286. package/mcp-server/src/tools/youtube/analytics.ts +1 -1
  287. package/mcp-server/src/tools/youtube/download.ts +1 -1
  288. package/mcp-server/src/tools/youtube/media.ts +1 -1
  289. package/mcp-server/src/tools/youtube/metadata.ts +1 -1
  290. package/mcp-server/src/tools/youtube/navigate.ts +1 -1
  291. package/mcp-server/src/tools/youtube/playlist.ts +1 -1
  292. package/mcp-server/src/tools/youtube/schedule.ts +1 -1
  293. package/mcp-server/src/tools/youtube/source.ts +1 -1
  294. package/mcp-server/src/tools/youtube/upload.ts +4 -12
  295. package/mcp-server/src/tui.ts +22 -9
  296. package/neural-interface/i18n/en.json +18 -0
  297. package/neural-interface/i18n/pt-BR.json +18 -0
  298. package/neural-interface/lib/agent-sandbox-elevation.js +102 -0
  299. package/neural-interface/lib/agent-sandbox-setting.js +57 -0
  300. package/neural-interface/lib/assistant-api.js +19 -0
  301. package/neural-interface/lib/assistant-route-gate-codex.js +3 -1
  302. package/neural-interface/lib/assistant-runtime.js +5 -1
  303. package/neural-interface/lib/codex-account-auth.js +4 -2
  304. package/neural-interface/lib/codex-accounts.js +14 -1
  305. package/neural-interface/lib/codex-settings-schema.js +5 -3
  306. package/neural-interface/lib/exec-loop-command.js +3 -1
  307. package/neural-interface/lib/install-details.js +23 -5
  308. package/neural-interface/lib/native-loop-providers.js +6 -1
  309. package/neural-interface/lib/provider-panel-terminal-auth.js +3 -2
  310. package/neural-interface/lib/session-indexer.js +3 -1
  311. package/neural-interface/lib/session-title-generator.js +2 -0
  312. package/neural-interface/package-lock.json +2 -2
  313. package/neural-interface/package.json +1 -1
  314. package/neural-interface/public/shared/account-menu.js +4 -1
  315. package/neural-interface/public/shared/account-usage.js +62 -2
  316. package/neural-interface/public/shared/account-view.js +2 -1
  317. package/neural-interface/public/shared/assistant/asst-account-menu.js +298 -0
  318. package/neural-interface/public/shared/assistant/asst-api.js +7 -0
  319. package/neural-interface/public/shared/assistant/asst-brain-picker.js +232 -75
  320. package/neural-interface/public/shared/assistant/asst-menu.js +204 -77
  321. package/neural-interface/public/shared/assistant/asst-model-chip.js +135 -0
  322. package/neural-interface/public/shared/assistant/asst-panel.js +39 -2
  323. package/neural-interface/public/shared/assistant/asst-styles.js +66 -17
  324. package/neural-interface/public/shared/cdx/cdx-panel.js +1 -1
  325. package/neural-interface/public/shared/cdx/cdx-styles.js +45 -5
  326. package/neural-interface/public/shared/cdx/cdx-tabs.js +3 -0
  327. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -1
  328. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +4 -1
  329. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +51 -2
  330. package/neural-interface/public/shared/showcase-privacy.js +2 -1
  331. package/neural-interface/public/shared/styles.css +3 -1
  332. package/neural-interface/public/shared/ui-claude-panel.js +50 -4
  333. package/neural-interface/public/shared/ui-explorer.js +2 -0
  334. package/neural-interface/public/shared/ui-sync.js +2 -0
  335. package/neural-interface/public/shared/ui-update.js +4 -2
  336. package/neural-interface/scripts/rebuild-pty.js +14 -2
  337. package/neural-interface/server.js +180 -86
  338. package/npm-shrinkwrap.json +4424 -0
  339. package/package.json +54 -2
  340. package/postinstall.js +61 -47
  341. package/preuninstall.js +8 -35
  342. package/setup.js +76 -44
  343. package/skills/synabun/modules/idea.md +1 -1
  344. package/skills/synabun/modules/schedule.md +1 -1
  345. package/updater.cmd +2 -2
  346. package/updater.mjs +97 -3
@@ -4,14 +4,23 @@
4
4
  // One body-level popover for every assistant menu (brain fields, model menu,
5
5
  // sessions, "⋯", route mode). Appended to <body> so it escapes the viewport's
6
6
  // paint containment; positioned with fixed coordinates next to its anchor and
7
- // clamped to the window. Only one menu is open at a time.
7
+ // kept inside the Assistant that holds the anchor (the sidepanel, or a tab's own
8
+ // root) and the window. Only one menu is open at a time.
8
9
  //
9
10
  // openMenu(anchor, { title, items, placement:'auto'|'above'|'below'|'prefer-below', filter,
10
11
  // width, role:'menu'|'listbox', label, className, onClose,
11
- // zIndex (a menu opened from a layer above the panel's own, e.g. Settings) })
12
+ // zIndex (a menu opened from a layer above the panel's own, e.g. Settings),
13
+ // footer, bounds, onEscape })
12
14
  // 'prefer-below' opens under the anchor whenever a usable list fits there (the
13
15
  // menu scrolls inside that room), even when more room is above: a menu opened
14
16
  // from Settings must not cover the Settings header. The panel's menus use 'auto'.
17
+ // `footer`: items under the list that stay in view while the list scrolls.
18
+ // Every menu is measured at the width it is shown at, never wider than its box less the margins,
19
+ // and opens where that box has room (the list scrolls inside it). The box is the Assistant
20
+ // panel or root around the anchor, else the window.
21
+ // `bounds`: an element (or () => DOMRect) to use as that box instead: the menu is then exactly
22
+ // `width` wide (or the box's room) and follows the box on resize.
23
+ // `onEscape`: returns true when it handled Esc (e.g. went back a view): the menu stays open.
15
24
  // Item kinds:
16
25
  // item (default) { label, desc, icon, tag, disabled, danger, keywords, onSelect }
17
26
  // radio { ...item, selected } → menuitemradio / option
@@ -21,10 +30,15 @@
21
30
  // info { label, value } → non-interactive row
22
31
  // tag { label } → non-interactive badge row
23
32
  // input { placeholder, value, onSubmit } → inline text field
33
+ // buttons { label, buttons: [item…] } → one row of compact items (a group)
34
+ // `render(button, { checked })` on an item draws its content instead of label/desc/tag.
35
+ // `className` on an item or an info row adds its own class.
36
+ // `keepOpen` on an item leaves the menu open when it is chosen.
24
37
  // `restoreFocus` on an item or input sends focus back to the anchor once it is chosen.
25
38
  // Keys: ↑/↓ move, Home/End jump, Enter/Space activate, Esc closes (focus back
26
39
  // to the anchor), Tab closes. The optional filter hides non-matching items and
27
- // any group left empty.
40
+ // any group left empty. The handle's update({ title, items, footer, focus }) redraws
41
+ // an open menu in place (focus: the data-id to land on).
28
42
 
29
43
  let _open = null;
30
44
  let _uid = 0;
@@ -87,11 +101,64 @@ export function currentMenu() {
87
101
  return _open?.menu || null;
88
102
  }
89
103
 
104
+ function itemButton(item, kind, { role, list, onActivate }) {
105
+ const btn = el('button', `asst-dd-item${item.danger ? ' danger' : ''}${item.className ? ` ${item.className}` : ''}`);
106
+ btn.type = 'button';
107
+ btn.tabIndex = -1;
108
+ btn.dataset.kind = kind;
109
+ if (item.id != null) btn.dataset.id = String(item.id);
110
+ btn.dataset.search = `${item.label || ''} ${item.desc || ''} ${item.keywords || ''}`.toLowerCase();
111
+ const checked = kind === 'radio' ? !!item.selected : kind === 'check' ? !!item.checked : false;
112
+ if (role === 'listbox') {
113
+ btn.setAttribute('role', 'option');
114
+ btn.setAttribute('aria-selected', checked ? 'true' : 'false');
115
+ } else if (kind === 'radio') {
116
+ btn.setAttribute('role', 'menuitemradio');
117
+ btn.setAttribute('aria-checked', checked ? 'true' : 'false');
118
+ } else if (kind === 'check') {
119
+ btn.setAttribute('role', 'menuitemcheckbox');
120
+ btn.setAttribute('aria-checked', checked ? 'true' : 'false');
121
+ } else {
122
+ btn.setAttribute('role', role === 'listbox' ? 'option' : 'menuitem');
123
+ }
124
+ if (item.disabled) btn.disabled = true;
125
+ if (typeof item.render === 'function') {
126
+ item.render(btn, { checked });
127
+ } else {
128
+ const lead = kind === 'radio' || kind === 'check'
129
+ ? `<span class="asst-dd-check" aria-hidden="true">${checked ? ICON_CHECK : ''}</span>`
130
+ : '';
131
+ btn.innerHTML = `${lead}${item.icon ? `<span class="asst-icon" aria-hidden="true"${item.color ? ` style="color:${esc(item.color)}"` : ''}>${item.icon}</span>` : ''}<span class="asst-dd-item-main"><span class="asst-dd-item-label">${esc(item.label)}</span>${item.desc ? `<span class="asst-dd-item-desc">${esc(item.desc)}</span>` : ''}</span>${item.tag ? `<span class="asst-dd-item-tag">${esc(item.tag)}</span>` : ''}${item.shortcut ? `<kbd class="asst-dd-kbd">${esc(item.shortcut)}</kbd>` : ''}`;
132
+ }
133
+ if (item.title) btn.title = item.title;
134
+ if (item.ariaLabel) btn.setAttribute('aria-label', item.ariaLabel);
135
+ btn.addEventListener('click', (e) => { e.stopPropagation(); if (!btn.disabled) onActivate(item, btn); });
136
+ btn.addEventListener('mousemove', () => {
137
+ if (btn.classList.contains('focused') || btn.disabled) return;
138
+ const menu = list.closest('.asst-dd-menu');
139
+ menu?.querySelectorAll('.asst-dd-item.focused').forEach(n => n.classList.remove('focused'));
140
+ btn.classList.add('focused');
141
+ // Hover never steals focus from the filter / inline input.
142
+ if (!document.activeElement?.classList?.contains('asst-dd-input')) { try { btn.focus({ preventScroll: true }); } catch { /* ignore */ } }
143
+ });
144
+ if (checked) btn.dataset.checked = '1';
145
+ return btn;
146
+ }
147
+
90
148
  function buildItems(list, items, { role, onActivate }) {
91
- const itemRole = role === 'listbox' ? 'option' : 'menuitem';
92
149
  for (const item of items || []) {
93
150
  const kind = itemKind(item);
94
151
  if (kind === 'separator') { list.appendChild(el('div', 'asst-dd-sep')).setAttribute('role', 'separator'); continue; }
152
+ if (kind === 'buttons') {
153
+ const row = el('div', 'asst-dd-btnrow');
154
+ row.setAttribute('role', 'group');
155
+ if (item.id != null) row.dataset.id = String(item.id);
156
+ if (item.ariaLabel || item.label) row.setAttribute('aria-label', item.ariaLabel || item.label);
157
+ if (item.label) row.appendChild(el('span', 'asst-dd-btnrow-label', item.label)).setAttribute('aria-hidden', 'true');
158
+ for (const button of item.buttons || []) row.appendChild(itemButton(button, itemKind(button), { role, list, onActivate }));
159
+ list.appendChild(row);
160
+ continue;
161
+ }
95
162
  if (kind === 'header') {
96
163
  const h = el('div', 'asst-dd-group');
97
164
  h.setAttribute('role', 'presentation');
@@ -100,7 +167,7 @@ function buildItems(list, items, { role, onActivate }) {
100
167
  continue;
101
168
  }
102
169
  if (kind === 'info' || kind === 'tag') {
103
- const row = el('div', kind === 'tag' ? 'asst-dd-tagrow' : 'asst-dd-info');
170
+ const row = el('div', `${kind === 'tag' ? 'asst-dd-tagrow' : 'asst-dd-info'}${item.className ? ` ${item.className}` : ''}`);
104
171
  row.setAttribute('role', 'presentation');
105
172
  row.innerHTML = kind === 'tag'
106
173
  ? `<span class="asst-dd-tag">${esc(item.label)}</span>`
@@ -121,42 +188,7 @@ function buildItems(list, items, { role, onActivate }) {
121
188
  list.appendChild(input);
122
189
  continue;
123
190
  }
124
- const btn = el('button', `asst-dd-item${item.danger ? ' danger' : ''}`);
125
- btn.type = 'button';
126
- btn.tabIndex = -1;
127
- btn.dataset.kind = kind;
128
- if (item.id != null) btn.dataset.id = String(item.id);
129
- btn.dataset.search = `${item.label || ''} ${item.desc || ''} ${item.keywords || ''}`.toLowerCase();
130
- const checked = kind === 'radio' ? !!item.selected : kind === 'check' ? !!item.checked : false;
131
- if (role === 'listbox') {
132
- btn.setAttribute('role', 'option');
133
- btn.setAttribute('aria-selected', checked ? 'true' : 'false');
134
- } else if (kind === 'radio') {
135
- btn.setAttribute('role', 'menuitemradio');
136
- btn.setAttribute('aria-checked', checked ? 'true' : 'false');
137
- } else if (kind === 'check') {
138
- btn.setAttribute('role', 'menuitemcheckbox');
139
- btn.setAttribute('aria-checked', checked ? 'true' : 'false');
140
- } else {
141
- btn.setAttribute('role', itemRole);
142
- }
143
- if (item.disabled) btn.disabled = true;
144
- const lead = kind === 'radio' || kind === 'check'
145
- ? `<span class="asst-dd-check" aria-hidden="true">${checked ? ICON_CHECK : ''}</span>`
146
- : '';
147
- btn.innerHTML = `${lead}${item.icon ? `<span class="asst-icon" aria-hidden="true"${item.color ? ` style="color:${esc(item.color)}"` : ''}>${item.icon}</span>` : ''}<span class="asst-dd-item-main"><span class="asst-dd-item-label">${esc(item.label)}</span>${item.desc ? `<span class="asst-dd-item-desc">${esc(item.desc)}</span>` : ''}</span>${item.tag ? `<span class="asst-dd-item-tag">${esc(item.tag)}</span>` : ''}${item.shortcut ? `<kbd class="asst-dd-kbd">${esc(item.shortcut)}</kbd>` : ''}`;
148
- if (item.title) btn.title = item.title;
149
- btn.addEventListener('click', (e) => { e.stopPropagation(); if (!btn.disabled) onActivate(item, btn); });
150
- btn.addEventListener('mousemove', () => {
151
- if (btn.classList.contains('focused') || btn.disabled) return;
152
- const menu = list.closest('.asst-dd-menu');
153
- menu?.querySelectorAll('.asst-dd-item.focused').forEach(n => n.classList.remove('focused'));
154
- btn.classList.add('focused');
155
- // Hover never steals focus from the filter / inline input.
156
- if (!document.activeElement?.classList?.contains('asst-dd-input')) { try { btn.focus({ preventScroll: true }); } catch { /* ignore */ } }
157
- });
158
- if (checked) btn.dataset.checked = '1';
159
- list.appendChild(btn);
191
+ list.appendChild(itemButton(item, kind, { role, list, onActivate }));
160
192
  }
161
193
  if (!list.querySelector('.asst-dd-item, .asst-dd-input, .asst-dd-info, .asst-dd-tagrow')) list.appendChild(el('div', 'asst-dd-empty', '—'));
162
194
  }
@@ -167,6 +199,8 @@ export function applyMenuFilter(list, query, emptyLabel = 'No matches') {
167
199
  const rows = [...list.children];
168
200
  for (const row of rows) {
169
201
  if (row.classList.contains('asst-dd-item')) row.hidden = !!q && !row.dataset.search.includes(q);
202
+ // A button row stays whole: shown while one of its buttons matches.
203
+ else if (row.classList.contains('asst-dd-btnrow')) row.hidden = !!q && ![...row.querySelectorAll('.asst-dd-item')].some(b => b.dataset.search.includes(q));
170
204
  else if (row.classList.contains('asst-dd-info') || row.classList.contains('asst-dd-tagrow')) row.hidden = !!q;
171
205
  }
172
206
  // Group headers: visible only when an item until the next header/separator is visible.
@@ -192,7 +226,7 @@ export function applyMenuFilter(list, query, emptyLabel = 'No matches') {
192
226
  const last = visible[visible.length - 1];
193
227
  if (last?.classList.contains('asst-dd-sep')) last.hidden = true;
194
228
  let empty = list.querySelector('.asst-dd-empty.asst-dd-noresults');
195
- const hasItem = rows.some(r => !r.hidden && (r.classList.contains('asst-dd-item') || r.classList.contains('asst-dd-input')));
229
+ const hasItem = rows.some(r => !r.hidden && (r.classList.contains('asst-dd-item') || r.classList.contains('asst-dd-btnrow') || r.classList.contains('asst-dd-input')));
196
230
  if (q && !hasItem) {
197
231
  if (!empty) { empty = el('div', 'asst-dd-empty asst-dd-noresults', emptyLabel); list.appendChild(empty); }
198
232
  empty.hidden = false;
@@ -204,7 +238,8 @@ const PREFER_BELOW_MIN = 220;
204
238
 
205
239
  /**
206
240
  * Whether a menu opens above its anchor (pure; sizes in px). `above` / `below`:
207
- * the room on each side of the anchor.
241
+ * the room on each side of the anchor; `anchorTop` / `viewportHeight`: the anchor's
242
+ * top in the box the menu stays inside, and that box's height.
208
243
  */
209
244
  export function menuOpensAbove(placement, { anchorTop, anchorHeight, menuHeight, above, below, viewportHeight }) {
210
245
  if (placement === 'above') return menuHeight <= above || above >= below;
@@ -214,37 +249,101 @@ export function menuOpensAbove(placement, { anchorTop, anchorHeight, menuHeight,
214
249
  return !(menuHeight <= below || below >= above);
215
250
  }
216
251
 
217
- function positionMenu(menu, anchor, placement) {
218
- const margin = 8;
219
- const gap = 6;
252
+ // A menu: never narrower than this while the window allows it; never shorter than these
253
+ // (a bounded menu holds a title and a footer around its list).
254
+ const BOUNDED_MIN_WIDTH = 248;
255
+ const MENU_MIN_HEIGHT = 96;
256
+ const BOUNDED_MIN_HEIGHT = 140;
257
+
258
+ /** The box a menu stays inside: its bounds clipped to the window (the window when it has none). */
259
+ function boundsBox(bounds) {
220
260
  const vw = window.innerWidth;
221
261
  const vh = window.innerHeight;
222
- const r = anchor.getBoundingClientRect();
223
- menu.style.maxHeight = '';
262
+ let r = null;
263
+ try { r = typeof bounds === 'function' ? bounds() : bounds?.getBoundingClientRect?.(); } catch { r = null; }
264
+ if (!r || !(r.width > 0) || !(r.height > 0)) return { left: 0, top: 0, right: vw, bottom: vh };
265
+ return { left: Math.max(0, r.left), top: Math.max(0, r.top), right: Math.min(vw, r.right), bottom: Math.min(vh, r.bottom) };
266
+ }
267
+
268
+ /** Where a menu without `bounds` stays: the Assistant around its anchor (the sidepanel, or a tab's own root). */
269
+ function assistantOf(anchor) {
270
+ return anchor.closest?.('.assistant-panel') || anchor.closest?.('.asst-root') || null;
271
+ }
272
+
273
+ /** The widest a menu may be in its box (pure; px): the box less the margins, never under the minimum the window allows. */
274
+ export function menuRoomWidth({ box, viewportWidth, margin = 8 }) {
275
+ return Math.round(Math.max(box.right - box.left - margin * 2, Math.min(BOUNDED_MIN_WIDTH, viewportWidth - margin * 2)));
276
+ }
277
+
278
+ /** A bounded menu's width (pure; px): `width` at most, its box less the margins, never under the minimum the window allows. */
279
+ export function boundedMenuWidth({ box, viewportWidth, width, margin = 8 }) {
280
+ return Math.round(Math.max(Math.min(width || 320, box.right - box.left - margin * 2), Math.min(BOUNDED_MIN_WIDTH, viewportWidth - margin * 2)));
281
+ }
282
+
283
+ /**
284
+ * Where a menu goes (pure; px; rects as getBoundingClientRect gives them): under the anchor or
285
+ * above it as `placement` decides from the room inside the box (menuOpensAbove), as tall as that
286
+ * room; right-aligned with `alignX` (default: the anchor's right edge), or starting at the anchor's
287
+ * left edge with `alignLeft`; always inside the box and the window.
288
+ * → { left, top, maxHeight (null: its own height fits), up }.
289
+ */
290
+ export function boundedMenuPlacement({ anchor, box, viewportWidth, viewportHeight, width, menuHeight, placement = 'below', alignX = null, alignLeft = false, minHeight = BOUNDED_MIN_HEIGHT, margin = 8, gap = 6 }) {
291
+ const below = box.bottom - margin - (anchor.bottom + gap);
292
+ const above = anchor.top - gap - (box.top + margin);
293
+ const up = menuOpensAbove(placement, { anchorTop: anchor.top - box.top, anchorHeight: anchor.bottom - anchor.top, menuHeight, above, below, viewportHeight: box.bottom - box.top });
294
+ const room = Math.max(minHeight, Math.floor(up ? above : below));
295
+ const height = Math.min(menuHeight, room);
296
+ let top = up ? anchor.top - gap - height : anchor.bottom + gap;
297
+ top = Math.max(margin, Math.min(top, viewportHeight - margin - height));
298
+ let left = alignLeft ? anchor.left : (alignX ?? anchor.right) - width;
299
+ left = Math.min(left, box.right - margin - width);
300
+ left = Math.max(left, box.left + margin);
301
+ left = Math.max(margin, Math.min(left, viewportWidth - margin - width));
302
+ return { left: Math.round(left), top: Math.round(top), maxHeight: menuHeight > room ? room : null, up };
303
+ }
304
+
305
+ /**
306
+ * Place a menu inside its box: its width is final before it is measured (the CSS caps read the
307
+ * box's room through --asst-dd-room; a bounded menu is set to its width), so what is shown is what
308
+ * was placed; then boundedMenuPlacement. The list scrolls inside, title and footer stay.
309
+ */
310
+ function positionMenu(menu, anchor, { placement, bounds, width }) {
311
+ const viewportWidth = window.innerWidth;
312
+ const viewportHeight = window.innerHeight;
313
+ const box = boundsBox(bounds || assistantOf(anchor));
314
+ if (bounds) {
315
+ menu.style.minWidth = '0';
316
+ menu.style.maxWidth = 'none';
317
+ menu.style.width = `${boundedMenuWidth({ box, viewportWidth, width })}px`;
318
+ menu.style.maxHeight = 'none';
319
+ } else {
320
+ menu.style.setProperty('--asst-dd-room', `${menuRoomWidth({ box, viewportWidth })}px`);
321
+ menu.style.maxHeight = '';
322
+ }
224
323
  menu.style.left = '0px';
225
324
  menu.style.top = '0px';
226
- const mw = Math.min(menu.offsetWidth, vw - margin * 2);
227
- let mh = menu.offsetHeight;
228
- const above = r.top - gap - margin;
229
- const below = vh - r.bottom - gap - margin;
230
- const up = menuOpensAbove(placement, { anchorTop: r.top, anchorHeight: r.height, menuHeight: mh, above, below, viewportHeight: vh });
231
- const room = Math.max(96, up ? above : below);
232
- if (mh > room) { menu.style.maxHeight = `${Math.floor(room)}px`; mh = Math.floor(room); }
233
- let top = up ? r.top - gap - mh : r.bottom + gap;
234
- top = Math.max(margin, Math.min(top, vh - margin - mh));
235
- let left = r.left;
236
- if (anchor.closest('.assistant-panel')) {
237
- // Sidepanel hugs the right screen edge: grow leftward from the click point
238
- // (or the anchor's right edge for keyboard opens).
239
- const x = _lastDown && anchor.contains(_lastDown.target) && performance.now() - _lastDown.at < 1500 ? _lastDown.x : r.right;
240
- left = x - mw;
325
+ // The sidepanel hugs the right screen edge: its menus grow leftward from the click point (or the
326
+ // anchor's right edge for keyboard opens), as a bounded menu does; elsewhere a menu starts at its anchor.
327
+ const inSidepanel = !!anchor.closest?.('.assistant-panel');
328
+ const clicked = inSidepanel && _lastDown && anchor.contains(_lastDown.target) && performance.now() - _lastDown.at < 1500;
329
+ const spot = boundedMenuPlacement({
330
+ anchor: anchor.getBoundingClientRect(), box, viewportWidth, viewportHeight,
331
+ width: menu.offsetWidth, menuHeight: menu.offsetHeight, placement,
332
+ alignX: clicked ? _lastDown.x : null, alignLeft: !bounds && !inSidepanel,
333
+ minHeight: bounds ? BOUNDED_MIN_HEIGHT : MENU_MIN_HEIGHT,
334
+ });
335
+ if (spot.maxHeight != null) menu.style.maxHeight = `${spot.maxHeight}px`;
336
+ menu.style.left = `${spot.left}px`;
337
+ menu.style.top = `${spot.top}px`;
338
+ menu.dataset.placement = spot.up ? 'above' : 'below';
339
+ }
340
+
341
+ /** A row its menu's width cuts short (one-line label or description) keeps its words in a tooltip. */
342
+ function titleCutRows(menu) {
343
+ for (const btn of menu.querySelectorAll('.asst-dd-item:not([title])')) {
344
+ const parts = [...btn.querySelectorAll('.asst-dd-item-label, .asst-dd-item-desc')];
345
+ if (parts.some(n => n.scrollWidth > n.clientWidth + 1)) btn.title = parts.map(n => n.textContent).filter(Boolean).join(' · ');
241
346
  }
242
- if (left + mw > vw - margin) left = vw - margin - mw;
243
- left = Math.max(margin, left);
244
- menu.style.left = `${Math.round(left)}px`;
245
- menu.style.top = `${Math.round(top)}px`;
246
- menu.style.maxWidth = `${vw - margin * 2}px`;
247
- menu.dataset.placement = up ? 'above' : 'below';
248
347
  }
249
348
 
250
349
  /**
@@ -262,8 +361,9 @@ export function openMenu(anchor, opts = {}) {
262
361
  menu.setAttribute('role', role);
263
362
  menu.id = `asst-menu-${++_uid}`;
264
363
  menu.tabIndex = -1;
364
+ let heading = null;
265
365
  if (title) {
266
- const heading = el('div', 'asst-dd-menu-title', title);
366
+ heading = el('div', 'asst-dd-menu-title', title);
267
367
  heading.id = `${menu.id}-title`;
268
368
  menu.setAttribute('aria-labelledby', heading.id);
269
369
  menu.appendChild(heading);
@@ -290,13 +390,25 @@ export function openMenu(anchor, opts = {}) {
290
390
  };
291
391
  if (opts.content) list.appendChild(opts.content);
292
392
  else buildItems(list, items, { role, onActivate: activate });
293
- if (width) menu.style.minWidth = opts.repositionOnResize ? `min(${width}px, calc(100vw - 16px))` : `${width}px`;
294
- if (width && opts.repositionOnResize) menu.style.width = `min(${width}px, calc(100vw - 16px))`;
393
+ // The footer stays under the list: only the list scrolls.
394
+ let foot = null;
395
+ const fillFooter = (footer) => {
396
+ if (!foot) { foot = el('div', 'asst-dd-foot'); menu.appendChild(foot); }
397
+ foot.replaceChildren();
398
+ if (Array.isArray(footer) && footer.length) buildItems(foot, footer, { role, onActivate: activate });
399
+ foot.hidden = !(Array.isArray(footer) && footer.length);
400
+ };
401
+ if (Array.isArray(opts.footer)) fillFooter(opts.footer);
402
+ // `width`: at least that wide (exactly, with repositionOnResize), never wider than the room positionMenu sets.
403
+ const capped = `min(${width}px, var(--asst-dd-room, calc(100vw - 16px)))`;
404
+ if (width) menu.style.minWidth = capped;
405
+ if (width && opts.repositionOnResize) menu.style.width = capped;
295
406
  document.body.appendChild(menu);
296
- positionMenu(menu, anchor, placement);
407
+ const place = () => { positionMenu(menu, anchor, { placement, bounds: opts.bounds || null, width }); titleCutRows(menu); };
408
+ place();
297
409
 
298
410
  if (filterInput) {
299
- filterInput.addEventListener('input', () => { applyMenuFilter(list, filterInput.value, opts.filterEmptyLabel); positionMenu(menu, anchor, placement); });
411
+ filterInput.addEventListener('input', () => { applyMenuFilter(list, filterInput.value, opts.filterEmptyLabel); place(); });
300
412
  }
301
413
 
302
414
  const onDocDown = (e) => {
@@ -311,7 +423,12 @@ export function openMenu(anchor, opts = {}) {
311
423
  const inInput = target?.classList?.contains('asst-dd-input');
312
424
  const nodes = focusables(menu);
313
425
  const idx = nodes.indexOf(document.activeElement);
314
- if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); closeMenu({ restoreFocus: true }); return; }
426
+ if (e.key === 'Escape') {
427
+ e.preventDefault(); e.stopPropagation();
428
+ if (typeof opts.onEscape === 'function' && opts.onEscape() === true) return;
429
+ closeMenu({ restoreFocus: true });
430
+ return;
431
+ }
315
432
  if (role === 'dialog') {
316
433
  if (e.key === 'Tab') {
317
434
  const fields = [...menu.querySelectorAll('button, input')].filter(node => !node.disabled && !node.hidden);
@@ -354,7 +471,7 @@ export function openMenu(anchor, opts = {}) {
354
471
  // Keep single-letter global keybinds out of an open menu.
355
472
  e.stopPropagation();
356
473
  };
357
- const onResize = () => opts.repositionOnResize ? positionMenu(menu, anchor, placement) : closeMenu();
474
+ const onResize = () => (opts.repositionOnResize || opts.bounds ? place() : closeMenu());
358
475
  const onBlur = () => closeMenu();
359
476
  document.addEventListener('mousedown', onDocDown, true);
360
477
  const keyTarget = role === 'dialog' ? window : document;
@@ -381,7 +498,17 @@ export function openMenu(anchor, opts = {}) {
381
498
  } else {
382
499
  setFocused(menu, selected || first || null);
383
500
  }
384
- return { el: menu, list, close: closeMenu, reposition: () => positionMenu(menu, anchor, placement) };
501
+ // Redraw in place (another view of the same menu): the anchor, the listeners and the focus stay in it.
502
+ const update = (next = {}) => {
503
+ if (!_open || _open.menu !== menu) return;
504
+ if (typeof next.title === 'string' && heading) heading.textContent = next.title;
505
+ if (Array.isArray(next.items)) { list.replaceChildren(); buildItems(list, next.items, { role, onActivate: activate }); list.scrollTop = 0; }
506
+ if ('footer' in next) fillFooter(next.footer);
507
+ place();
508
+ const wanted = next.focus != null ? [...menu.querySelectorAll('.asst-dd-item')].find(n => n.dataset.id === String(next.focus) && !n.disabled) : null;
509
+ setFocused(menu, wanted || list.querySelector('.asst-dd-item[data-checked="1"]:not([disabled])') || focusables(menu)[0] || null);
510
+ };
511
+ return { el: menu, list, close: closeMenu, reposition: place, update };
385
512
  }
386
513
 
387
514
  /** Toggle helper for anchor buttons: closes when this anchor's menu is open. */
@@ -0,0 +1,135 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Assistant — what the composer's model chip says
3
+ // ═══════════════════════════════════════════
4
+ // The model as a short human name from its catalog label ("Opus 5.5 (1M
5
+ // context)" → "Opus 5.5", "Default (recommended) — Opus 5.5" → "Opus 5.5"),
6
+ // a context tag for a "[1m]" / "[extended]" variant ("1M"), and the forms the
7
+ // chip falls back to when the composer row has no room, longest first: the tag
8
+ // goes first, then the account cue, then the API badge, then the name shortens
9
+ // by whole words, to a form that still names this model alone when there is one
10
+ // ("Opus 5.5" → "Opus", "GPT-6-Luna" → "Luna"). Never a cut word.
11
+ // asst-brain-picker.js draws the first form that fits. DOM-free: node --test loads it.
12
+
13
+ const VARIANT = /\[(1m|extended)\]$/i;
14
+ const VERSION = /^v?\d+(?:\.\d+)*[a-z]?$/i;
15
+
16
+ function str(v) {
17
+ return v == null ? '' : String(v).trim();
18
+ }
19
+
20
+ /** The id without its provider ("anthropic/…"), legacy ":<window>" and variant suffix. */
21
+ function bareId(id) {
22
+ const tail = str(id).split('/').pop() || '';
23
+ return tail.replace(/:\d+$/, '').replace(VARIANT, '');
24
+ }
25
+
26
+ /**
27
+ * The model's short name: { name, raw }. The label's own words without what the
28
+ * chip says otherwise (parentheses: the context, "recommended"); after a dash, the
29
+ * model it runs as. No label (or the id itself): the bare id, `raw` (shown mono).
30
+ */
31
+ export function modelChipName(label, id) {
32
+ let text = str(label);
33
+ if (text && text !== str(id)) {
34
+ const runsAs = text.split(/\s+[—–]\s+/).pop();
35
+ text = str(runsAs.replace(/\s*\([^)]*\)/g, '').replace(/\s+/g, ' '));
36
+ if (text) return { name: text, raw: false };
37
+ }
38
+ const name = bareId(id) || str(id);
39
+ return { name, raw: true };
40
+ }
41
+
42
+ /** "1M", "400K": a context window in tokens, never rounded up. */
43
+ function compactTokens(n) {
44
+ const v = Number(n);
45
+ if (!Number.isFinite(v) || v <= 0) return '';
46
+ if (v >= 1e6) return `${Math.floor(v / 1e5) / 10}M`;
47
+ if (v >= 1e3) return `${Math.floor(v / 1e3)}K`;
48
+ return String(v);
49
+ }
50
+
51
+ /**
52
+ * What tells a context variant from its model ("opus[1m]" → "1M"): its window when
53
+ * the catalog knows it, else "1M" for "[1m]", else the label's own word ("extended").
54
+ * '' for a model that is not a variant.
55
+ */
56
+ export function modelChipTag(id, { contextWindow = null, label = '' } = {}) {
57
+ const m = str(id).match(VARIANT);
58
+ if (!m) return '';
59
+ const tokens = compactTokens(contextWindow);
60
+ if (tokens) return tokens;
61
+ if (m[1].toLowerCase() === '1m') return '1M';
62
+ const note = str(label).match(/\(([^)]*)\)\s*$/);
63
+ return note ? str(note[1].replace(/\bcontext\b/i, '')) : '';
64
+ }
65
+
66
+ // "GPT-6.1-Sol" → ['GPT', '6.1', 'Sol'] with the separators kept: whole words in, whole words out.
67
+ function parts(name) {
68
+ return str(name).match(/[^\s-]+|[\s-]+/g) || [];
69
+ }
70
+ const isSep = (p) => /^[\s-]+$/.test(p);
71
+ const wordsOf = (name) => parts(name).filter(p => !isSep(p)).map(w => w.toLowerCase());
72
+
73
+ /** Whether `words` run in that order, whole, somewhere in `other` ("opus" in "Opus 5.5"). */
74
+ function within(words, other) {
75
+ for (let i = 0; i + words.length <= other.length; i += 1) {
76
+ if (words.every((w, j) => other[i + j] === w)) return true;
77
+ }
78
+ return false;
79
+ }
80
+
81
+ /**
82
+ * The name's shorter forms, whole words only, longest first: `unique` still names this
83
+ * model alone among `names` (the short names of every model the provider lists: "Opus"
84
+ * for "Opus 5.5", "GPT-6.1" for "GPT-6.1-Sol", "Luna" for "GPT-6-Luna" when every
85
+ * "GPT-6" is another model too), `other` drops trailing words whatever they lose
86
+ * ("Sonnet" for "Sonnet 5.5" beside "Sonnet 5"; "GPT-6"). A form never starts with a
87
+ * version ("5.5"), and a suffix is tried only when no shorter prefix is unique.
88
+ */
89
+ export function modelChipShortNames(name, names = []) {
90
+ const all = parts(name);
91
+ const own = wordsOf(name);
92
+ const others = [...new Set((Array.isArray(names) ? names : []).map(str).filter(n => n && n.toLowerCase() !== str(name).toLowerCase()))].map(wordsOf);
93
+ const unique = (form) => { const w = wordsOf(form); return !others.some(o => within(w, o)); };
94
+ const prefixes = [];
95
+ const suffixes = [];
96
+ for (let n = own.length - 1; n >= 1; n -= 1) {
97
+ let seen = 0;
98
+ const cut = all.findIndex(p => !isSep(p) && ++seen > n);
99
+ prefixes.push(all.slice(0, cut).join('').replace(/[\s-]+$/, ''));
100
+ let skip = own.length - n;
101
+ const from = all.findIndex(p => !isSep(p) && skip-- === 0);
102
+ if (!VERSION.test(all[from])) suffixes.push(all.slice(from).join(''));
103
+ }
104
+ const lossless = prefixes.filter(unique);
105
+ const picked = lossless.length ? lossless : suffixes.filter(unique);
106
+ return { unique: picked, other: prefixes.filter(p => !picked.includes(p)) };
107
+ }
108
+
109
+ /**
110
+ * The chip's forms, longest first: [{ name, tag, cue, api }] (tag / cue / api: shown
111
+ * or not). `tag`, `cue` and `api` say whether the chip has one at all; `names` are the
112
+ * provider's models (modelChipShortNames). What goes, in order: the tag, the account
113
+ * cue, the API badge, then the name. A shorter name that still names this model alone
114
+ * loses nothing, so it is tried before the cue and before the badge go; one that
115
+ * loses something only after.
116
+ */
117
+ export function modelChipForms({ name, names = [], tag = false, cue = false, api = false } = {}) {
118
+ const full = str(name);
119
+ const out = [];
120
+ const seen = new Set();
121
+ const add = (n, t, c, a) => {
122
+ const form = { name: n, tag: !!(t && tag), cue: !!(c && cue), api: !!(a && api) };
123
+ const key = JSON.stringify(form);
124
+ if (!seen.has(key)) { seen.add(key); out.push(form); }
125
+ };
126
+ const short = full ? modelChipShortNames(full, names) : { unique: [], other: [] };
127
+ add(full, true, true, true);
128
+ add(full, false, true, true);
129
+ for (const u of short.unique) add(u, false, true, true);
130
+ add(full, false, false, true);
131
+ for (const u of short.unique) add(u, false, false, true);
132
+ add(full, false, false, false);
133
+ for (const n of [...short.unique, ...short.other]) add(n, false, false, false);
134
+ return out;
135
+ }
@@ -64,7 +64,7 @@ import {
64
64
  import {
65
65
  closeAssistantSession, dispatchAssistantRun, escalateAssistantRun, focusAssistantRun, getAssistantRun,
66
66
  fetchManageCatalog, getAssistantRunResult, getAssistantSession, getAssistantSessionUsage, getDesktopStatus, getRouting, killAllAssistantRuns, removeAssistantRun, clearFinishedAssistantRuns,
67
- listAssistantSessions, patchAssistantSession, patchHiddenModels, stopAssistantRun, uploadAssistantAttachment,
67
+ listAssistantSessions, patchAssistantSession, patchHiddenModels, setAdminSandbox, stopAssistantRun, uploadAssistantAttachment,
68
68
  } from './asst-api.js';
69
69
 
70
70
  const ICON_SESSIONS = '<svg viewBox="0 0 24 24"><path d="M3 6h18M3 12h18M3 18h12"/></svg>';
@@ -154,8 +154,9 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
154
154
  engine: '',
155
155
  sdkVersion: '',
156
156
  routing: null, // server view { mode, effectiveMode, defaultMode, askBelow }
157
- features: { routing: null, computer: null, testDensity: null, rename: null }, // null = unknown yet
157
+ features: { routing: null, computer: null, testDensity: null, rename: null, adminSandbox: null }, // null = unknown yet
158
158
  testDensityEffective: null, // server view: the level in effect
159
+ adminSandbox: null, // server view: the global Admin sandbox setting (every agent)
159
160
  brainCapabilities: null,
160
161
  computerEffective: st.computerUse === true,
161
162
  desktopSupported: null,
@@ -583,6 +584,9 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
583
584
  // Test density sits right after the permission mode; hidden on a server that does not know it.
584
585
  getTestDensity: () => testDensityView(),
585
586
  onTestDensity: (level) => setTestDensity(level),
587
+ // The global Admin sandbox switch, in the permission menu; hidden on a server that does not serve it.
588
+ getAdminSandbox: () => adminSandboxView(),
589
+ onAdminSandbox: (enabled) => changeAdminSandbox(enabled),
586
590
  // The registered projects arrived: their labels name the project in the fields and the "Fix … in" suggestion.
587
591
  onProjects: () => { picker.render(); renderer.refreshEmpty(); },
588
592
  });
@@ -950,7 +954,9 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
950
954
  if (typeof meta.features.computer === 'boolean') st.features.computer = meta.features.computer;
951
955
  if (typeof meta.features.testDensity === 'boolean') st.features.testDensity = meta.features.testDensity;
952
956
  if (typeof meta.features.rename === 'boolean') st.features.rename = meta.features.rename;
957
+ if (typeof meta.features.adminSandbox === 'boolean') st.features.adminSandbox = meta.features.adminSandbox;
953
958
  }
959
+ if (typeof meta.adminSandbox === 'boolean') st.adminSandbox = meta.adminSandbox;
954
960
  if ('testDensityExplicit' in meta) st.testDensity = normalizeTestDensity(meta.testDensityExplicit, null);
955
961
  if (meta.testDensity) st.testDensityEffective = normalizeTestDensity(meta.testDensity);
956
962
  if (typeof meta.computerUse === 'boolean') st.computerEffective = meta.computerUse;
@@ -1066,6 +1072,31 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
1066
1072
  if (!quiet) renderer.appendStatus(tt('assistant.status.testDensity', 'Test density: {level}', { level: testDensityLabel(next, t) }));
1067
1073
  }
1068
1074
 
1075
+ /** The global Admin sandbox setting (true / false), or null while the server does not serve it. */
1076
+ function adminSandboxView() {
1077
+ if (st.features.adminSandbox !== true || typeof st.adminSandbox !== 'boolean') return null;
1078
+ return st.adminSandbox;
1079
+ }
1080
+
1081
+ /** Every agent SynaBun starts follows it; every window hears the change (sync:agents:admin-sandbox-changed). */
1082
+ async function changeAdminSandbox(enabled) {
1083
+ const before = st.adminSandbox;
1084
+ st.adminSandbox = enabled === true;
1085
+ picker.render();
1086
+ try {
1087
+ const view = await setAdminSandbox(st.adminSandbox);
1088
+ if (typeof view?.adminSandbox === 'boolean') st.adminSandbox = view.adminSandbox;
1089
+ picker.render();
1090
+ renderer.appendStatus(st.adminSandbox
1091
+ ? tt('assistant.adminSandbox.statusOn', 'Admin sandbox: on. On Windows, Codex may ask for administrator approval once per account to set up its sandbox.')
1092
+ : tt('assistant.adminSandbox.statusOff', 'Admin sandbox: off. No agent SynaBun starts asks for administrator approval for a sandbox.'));
1093
+ } catch (err) {
1094
+ st.adminSandbox = before;
1095
+ picker.render();
1096
+ toast(err?.message || String(err));
1097
+ }
1098
+ }
1099
+
1069
1100
  /** Fresh tabs inherit the last-used test density (like the route mode). */
1070
1101
  function applyLastUsedTestDensity() {
1071
1102
  if (saved || st.features.testDensity !== true || st.testDensity != null) return;
@@ -1879,6 +1910,12 @@ export function mountAssistant(viewport, { sessionId, brain, host = {}, session
1879
1910
  st.routing = { ...(st.routing || {}), defaultMode: def, effectiveMode: st.routeMode || def };
1880
1911
  routeChip.render();
1881
1912
  }));
1913
+ // The global Admin sandbox setting changed in some window (this one included): every panel shows it.
1914
+ unsubs.push(on('sync:agents:admin-sandbox-changed', (msg) => {
1915
+ if (typeof msg?.adminSandbox !== 'boolean' || msg.adminSandbox === st.adminSandbox) return;
1916
+ st.adminSandbox = msg.adminSandbox;
1917
+ picker.render();
1918
+ }));
1882
1919
  unsubs.push(on('session:info', () => updateGuest()));
1883
1920
  unsubs.push(on('permissions:changed', () => updateGuest()));
1884
1921