synabun 2.0.5 → 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 (527) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +2 -1
  3. package/THIRD-PARTY-LICENSES.md +13 -0
  4. package/docs/whatsapp.md +151 -16
  5. package/hooks/claude-code/post-plan.mjs +6 -30
  6. package/hooks/claude-code/session-start.mjs +1 -4
  7. package/hooks/claude-code/shared.mjs +112 -75
  8. package/lib/child-path.js +303 -0
  9. package/lib/claude-hooks.js +2 -1
  10. package/lib/data-home-migration.js +2 -0
  11. package/lib/desktop-app.js +265 -0
  12. package/lib/external-tools.js +7 -3
  13. package/lib/log-redact.js +102 -0
  14. package/lib/package-processes.js +670 -0
  15. package/lib/packaged-runtime.js +138 -11
  16. package/lib/runtime-dependencies.js +225 -0
  17. package/mcp-server/dist/http.d.ts +3 -1
  18. package/mcp-server/dist/http.d.ts.map +1 -1
  19. package/mcp-server/dist/http.js +15 -4
  20. package/mcp-server/dist/http.js.map +1 -1
  21. package/mcp-server/dist/index.d.ts.map +1 -1
  22. package/mcp-server/dist/index.js +9 -6
  23. package/mcp-server/dist/index.js.map +1 -1
  24. package/mcp-server/dist/services/browser-bench.d.ts +1 -1
  25. package/mcp-server/dist/services/browser-bench.d.ts.map +1 -1
  26. package/mcp-server/dist/services/browser-bench.js +1 -1
  27. package/mcp-server/dist/services/browser-bench.js.map +1 -1
  28. package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
  29. package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
  30. package/mcp-server/dist/services/budget-judgments.js +153 -0
  31. package/mcp-server/dist/services/budget-judgments.js.map +1 -0
  32. package/mcp-server/dist/services/categories.d.ts +35 -2
  33. package/mcp-server/dist/services/categories.d.ts.map +1 -1
  34. package/mcp-server/dist/services/categories.js +77 -33
  35. package/mcp-server/dist/services/categories.js.map +1 -1
  36. package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
  37. package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
  38. package/mcp-server/dist/services/codex-config-heal.js +41 -4
  39. package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
  40. package/mcp-server/dist/services/desktop-bench.d.ts +7 -7
  41. package/mcp-server/dist/services/desktop-bench.d.ts.map +1 -1
  42. package/mcp-server/dist/services/desktop-bench.js +1 -1
  43. package/mcp-server/dist/services/desktop-bench.js.map +1 -1
  44. package/mcp-server/dist/services/identity.d.ts +11 -0
  45. package/mcp-server/dist/services/identity.d.ts.map +1 -1
  46. package/mcp-server/dist/services/identity.js +24 -0
  47. package/mcp-server/dist/services/identity.js.map +1 -1
  48. package/mcp-server/dist/services/neural-interface.d.ts +13 -0
  49. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  50. package/mcp-server/dist/services/neural-interface.js +34 -2
  51. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  52. package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
  53. package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
  54. package/mcp-server/dist/services/typesafe-config.js +4 -1
  55. package/mcp-server/dist/services/typesafe-config.js.map +1 -1
  56. package/mcp-server/dist/tools/agents.d.ts +11 -8
  57. package/mcp-server/dist/tools/agents.d.ts.map +1 -1
  58. package/mcp-server/dist/tools/agents.js +35 -14
  59. package/mcp-server/dist/tools/agents.js.map +1 -1
  60. package/mcp-server/dist/tools/bluesky.js +1 -1
  61. package/mcp-server/dist/tools/bluesky.js.map +1 -1
  62. package/mcp-server/dist/tools/browser-advanced.d.ts +1 -1
  63. package/mcp-server/dist/tools/browser-advanced.d.ts.map +1 -1
  64. package/mcp-server/dist/tools/browser-advanced.js +1 -1
  65. package/mcp-server/dist/tools/browser-advanced.js.map +1 -1
  66. package/mcp-server/dist/tools/browser-batch.d.ts +1 -1
  67. package/mcp-server/dist/tools/browser-batch.d.ts.map +1 -1
  68. package/mcp-server/dist/tools/browser-batch.js +1 -1
  69. package/mcp-server/dist/tools/browser-batch.js.map +1 -1
  70. package/mcp-server/dist/tools/browser-cheatsheet.d.ts +1 -1
  71. package/mcp-server/dist/tools/browser-cheatsheet.d.ts.map +1 -1
  72. package/mcp-server/dist/tools/browser-cheatsheet.js +1 -1
  73. package/mcp-server/dist/tools/browser-cheatsheet.js.map +1 -1
  74. package/mcp-server/dist/tools/browser-interact.d.ts +1 -1
  75. package/mcp-server/dist/tools/browser-interact.d.ts.map +1 -1
  76. package/mcp-server/dist/tools/browser-interact.js +1 -1
  77. package/mcp-server/dist/tools/browser-interact.js.map +1 -1
  78. package/mcp-server/dist/tools/browser-navigate.d.ts +1 -1
  79. package/mcp-server/dist/tools/browser-navigate.d.ts.map +1 -1
  80. package/mcp-server/dist/tools/browser-navigate.js +1 -1
  81. package/mcp-server/dist/tools/browser-navigate.js.map +1 -1
  82. package/mcp-server/dist/tools/browser-observe.d.ts +1 -1
  83. package/mcp-server/dist/tools/browser-observe.d.ts.map +1 -1
  84. package/mcp-server/dist/tools/browser-observe.js +1 -1
  85. package/mcp-server/dist/tools/browser-observe.js.map +1 -1
  86. package/mcp-server/dist/tools/card-tools.d.ts +1 -1
  87. package/mcp-server/dist/tools/card-tools.d.ts.map +1 -1
  88. package/mcp-server/dist/tools/card-tools.js +1 -1
  89. package/mcp-server/dist/tools/card-tools.js.map +1 -1
  90. package/mcp-server/dist/tools/category.d.ts +1 -1
  91. package/mcp-server/dist/tools/category.d.ts.map +1 -1
  92. package/mcp-server/dist/tools/category.js +5 -1
  93. package/mcp-server/dist/tools/category.js.map +1 -1
  94. package/mcp-server/dist/tools/choice.d.ts +1 -1
  95. package/mcp-server/dist/tools/choice.d.ts.map +1 -1
  96. package/mcp-server/dist/tools/choice.js +1 -1
  97. package/mcp-server/dist/tools/choice.js.map +1 -1
  98. package/mcp-server/dist/tools/computer.d.ts +1 -1
  99. package/mcp-server/dist/tools/computer.d.ts.map +1 -1
  100. package/mcp-server/dist/tools/computer.js +1 -1
  101. package/mcp-server/dist/tools/computer.js.map +1 -1
  102. package/mcp-server/dist/tools/discord-channel.d.ts +1 -1
  103. package/mcp-server/dist/tools/discord-channel.d.ts.map +1 -1
  104. package/mcp-server/dist/tools/discord-channel.js +1 -1
  105. package/mcp-server/dist/tools/discord-channel.js.map +1 -1
  106. package/mcp-server/dist/tools/discord-guild.d.ts +1 -1
  107. package/mcp-server/dist/tools/discord-guild.d.ts.map +1 -1
  108. package/mcp-server/dist/tools/discord-guild.js +1 -1
  109. package/mcp-server/dist/tools/discord-guild.js.map +1 -1
  110. package/mcp-server/dist/tools/discord-member.d.ts +1 -1
  111. package/mcp-server/dist/tools/discord-member.d.ts.map +1 -1
  112. package/mcp-server/dist/tools/discord-member.js +1 -1
  113. package/mcp-server/dist/tools/discord-member.js.map +1 -1
  114. package/mcp-server/dist/tools/discord-message.d.ts +1 -1
  115. package/mcp-server/dist/tools/discord-message.d.ts.map +1 -1
  116. package/mcp-server/dist/tools/discord-message.js +1 -1
  117. package/mcp-server/dist/tools/discord-message.js.map +1 -1
  118. package/mcp-server/dist/tools/discord-onboarding.d.ts +1 -1
  119. package/mcp-server/dist/tools/discord-onboarding.d.ts.map +1 -1
  120. package/mcp-server/dist/tools/discord-onboarding.js +1 -1
  121. package/mcp-server/dist/tools/discord-onboarding.js.map +1 -1
  122. package/mcp-server/dist/tools/discord-role.d.ts +1 -1
  123. package/mcp-server/dist/tools/discord-role.d.ts.map +1 -1
  124. package/mcp-server/dist/tools/discord-role.js +1 -1
  125. package/mcp-server/dist/tools/discord-role.js.map +1 -1
  126. package/mcp-server/dist/tools/discord-thread.d.ts +1 -1
  127. package/mcp-server/dist/tools/discord-thread.d.ts.map +1 -1
  128. package/mcp-server/dist/tools/discord-thread.js +1 -1
  129. package/mcp-server/dist/tools/discord-thread.js.map +1 -1
  130. package/mcp-server/dist/tools/discord-webhook.d.ts +1 -1
  131. package/mcp-server/dist/tools/discord-webhook.d.ts.map +1 -1
  132. package/mcp-server/dist/tools/discord-webhook.js +1 -1
  133. package/mcp-server/dist/tools/discord-webhook.js.map +1 -1
  134. package/mcp-server/dist/tools/fb-groups.d.ts +1 -1
  135. package/mcp-server/dist/tools/fb-groups.d.ts.map +1 -1
  136. package/mcp-server/dist/tools/fb-groups.js +1 -1
  137. package/mcp-server/dist/tools/fb-groups.js.map +1 -1
  138. package/mcp-server/dist/tools/forget.d.ts +1 -1
  139. package/mcp-server/dist/tools/forget.d.ts.map +1 -1
  140. package/mcp-server/dist/tools/forget.js +1 -1
  141. package/mcp-server/dist/tools/forget.js.map +1 -1
  142. package/mcp-server/dist/tools/git-tools.d.ts +1 -1
  143. package/mcp-server/dist/tools/git-tools.d.ts.map +1 -1
  144. package/mcp-server/dist/tools/git-tools.js +1 -1
  145. package/mcp-server/dist/tools/git-tools.js.map +1 -1
  146. package/mcp-server/dist/tools/gsc/experience.d.ts +1 -1
  147. package/mcp-server/dist/tools/gsc/experience.d.ts.map +1 -1
  148. package/mcp-server/dist/tools/gsc/experience.js +1 -1
  149. package/mcp-server/dist/tools/gsc/experience.js.map +1 -1
  150. package/mcp-server/dist/tools/gsc/indexing.d.ts +1 -1
  151. package/mcp-server/dist/tools/gsc/indexing.d.ts.map +1 -1
  152. package/mcp-server/dist/tools/gsc/indexing.js +1 -1
  153. package/mcp-server/dist/tools/gsc/indexing.js.map +1 -1
  154. package/mcp-server/dist/tools/gsc/inspect.d.ts +1 -1
  155. package/mcp-server/dist/tools/gsc/inspect.d.ts.map +1 -1
  156. package/mcp-server/dist/tools/gsc/inspect.js +1 -1
  157. package/mcp-server/dist/tools/gsc/inspect.js.map +1 -1
  158. package/mcp-server/dist/tools/gsc/navigate.d.ts +1 -1
  159. package/mcp-server/dist/tools/gsc/navigate.d.ts.map +1 -1
  160. package/mcp-server/dist/tools/gsc/navigate.js +1 -1
  161. package/mcp-server/dist/tools/gsc/navigate.js.map +1 -1
  162. package/mcp-server/dist/tools/gsc/performance.d.ts +1 -1
  163. package/mcp-server/dist/tools/gsc/performance.d.ts.map +1 -1
  164. package/mcp-server/dist/tools/gsc/performance.js +1 -1
  165. package/mcp-server/dist/tools/gsc/performance.js.map +1 -1
  166. package/mcp-server/dist/tools/gsc/settings.d.ts +1 -1
  167. package/mcp-server/dist/tools/gsc/settings.d.ts.map +1 -1
  168. package/mcp-server/dist/tools/gsc/settings.js +1 -1
  169. package/mcp-server/dist/tools/gsc/settings.js.map +1 -1
  170. package/mcp-server/dist/tools/image-tools.d.ts +1 -1
  171. package/mcp-server/dist/tools/image-tools.d.ts.map +1 -1
  172. package/mcp-server/dist/tools/image-tools.js +1 -1
  173. package/mcp-server/dist/tools/image-tools.js.map +1 -1
  174. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +1 -1
  175. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +1 -1
  176. package/mcp-server/dist/tools/leonardo-browser-tools.js +1 -1
  177. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +1 -1
  178. package/mcp-server/dist/tools/loop.d.ts +1 -1
  179. package/mcp-server/dist/tools/loop.d.ts.map +1 -1
  180. package/mcp-server/dist/tools/loop.js +1 -1
  181. package/mcp-server/dist/tools/loop.js.map +1 -1
  182. package/mcp-server/dist/tools/memories.d.ts +1 -1
  183. package/mcp-server/dist/tools/memories.d.ts.map +1 -1
  184. package/mcp-server/dist/tools/memories.js +1 -1
  185. package/mcp-server/dist/tools/memories.js.map +1 -1
  186. package/mcp-server/dist/tools/morelogin.d.ts +1 -1
  187. package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
  188. package/mcp-server/dist/tools/morelogin.js +1 -1
  189. package/mcp-server/dist/tools/morelogin.js.map +1 -1
  190. package/mcp-server/dist/tools/profile.d.ts +1 -1
  191. package/mcp-server/dist/tools/profile.d.ts.map +1 -1
  192. package/mcp-server/dist/tools/profile.js +1 -1
  193. package/mcp-server/dist/tools/profile.js.map +1 -1
  194. package/mcp-server/dist/tools/recall.d.ts +1 -1
  195. package/mcp-server/dist/tools/recall.d.ts.map +1 -1
  196. package/mcp-server/dist/tools/recall.js +1 -1
  197. package/mcp-server/dist/tools/recall.js.map +1 -1
  198. package/mcp-server/dist/tools/reflect.d.ts +1 -1
  199. package/mcp-server/dist/tools/reflect.d.ts.map +1 -1
  200. package/mcp-server/dist/tools/reflect.js +1 -1
  201. package/mcp-server/dist/tools/reflect.js.map +1 -1
  202. package/mcp-server/dist/tools/remember.d.ts +1 -1
  203. package/mcp-server/dist/tools/remember.d.ts.map +1 -1
  204. package/mcp-server/dist/tools/remember.js +13 -11
  205. package/mcp-server/dist/tools/remember.js.map +1 -1
  206. package/mcp-server/dist/tools/restore.d.ts +1 -1
  207. package/mcp-server/dist/tools/restore.d.ts.map +1 -1
  208. package/mcp-server/dist/tools/restore.js +1 -1
  209. package/mcp-server/dist/tools/restore.js.map +1 -1
  210. package/mcp-server/dist/tools/style-guide-tools.d.ts +1 -1
  211. package/mcp-server/dist/tools/style-guide-tools.d.ts.map +1 -1
  212. package/mcp-server/dist/tools/style-guide-tools.js +1 -1
  213. package/mcp-server/dist/tools/style-guide-tools.js.map +1 -1
  214. package/mcp-server/dist/tools/sync.d.ts +1 -1
  215. package/mcp-server/dist/tools/sync.d.ts.map +1 -1
  216. package/mcp-server/dist/tools/sync.js +1 -1
  217. package/mcp-server/dist/tools/sync.js.map +1 -1
  218. package/mcp-server/dist/tools/tictactoe-tools.d.ts +1 -1
  219. package/mcp-server/dist/tools/tictactoe-tools.d.ts.map +1 -1
  220. package/mcp-server/dist/tools/tictactoe-tools.js +1 -1
  221. package/mcp-server/dist/tools/tictactoe-tools.js.map +1 -1
  222. package/mcp-server/dist/tools/utils.d.ts +1 -1
  223. package/mcp-server/dist/tools/utils.d.ts.map +1 -1
  224. package/mcp-server/dist/tools/utils.js +1 -1
  225. package/mcp-server/dist/tools/utils.js.map +1 -1
  226. package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
  227. package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
  228. package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
  229. package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
  230. package/mcp-server/dist/tools/whiteboard-tools.d.ts +3 -3
  231. package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
  232. package/mcp-server/dist/tools/whiteboard-tools.js +1 -1
  233. package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
  234. package/mcp-server/dist/tools/youtube/analytics.d.ts +1 -1
  235. package/mcp-server/dist/tools/youtube/analytics.d.ts.map +1 -1
  236. package/mcp-server/dist/tools/youtube/analytics.js +1 -1
  237. package/mcp-server/dist/tools/youtube/analytics.js.map +1 -1
  238. package/mcp-server/dist/tools/youtube/download.d.ts +1 -1
  239. package/mcp-server/dist/tools/youtube/download.d.ts.map +1 -1
  240. package/mcp-server/dist/tools/youtube/download.js +1 -1
  241. package/mcp-server/dist/tools/youtube/download.js.map +1 -1
  242. package/mcp-server/dist/tools/youtube/media.d.ts +1 -1
  243. package/mcp-server/dist/tools/youtube/media.d.ts.map +1 -1
  244. package/mcp-server/dist/tools/youtube/media.js +1 -1
  245. package/mcp-server/dist/tools/youtube/media.js.map +1 -1
  246. package/mcp-server/dist/tools/youtube/metadata.d.ts +1 -1
  247. package/mcp-server/dist/tools/youtube/metadata.d.ts.map +1 -1
  248. package/mcp-server/dist/tools/youtube/metadata.js +1 -1
  249. package/mcp-server/dist/tools/youtube/metadata.js.map +1 -1
  250. package/mcp-server/dist/tools/youtube/navigate.d.ts +1 -1
  251. package/mcp-server/dist/tools/youtube/navigate.d.ts.map +1 -1
  252. package/mcp-server/dist/tools/youtube/navigate.js +1 -1
  253. package/mcp-server/dist/tools/youtube/navigate.js.map +1 -1
  254. package/mcp-server/dist/tools/youtube/playlist.d.ts +1 -1
  255. package/mcp-server/dist/tools/youtube/playlist.d.ts.map +1 -1
  256. package/mcp-server/dist/tools/youtube/playlist.js +1 -1
  257. package/mcp-server/dist/tools/youtube/playlist.js.map +1 -1
  258. package/mcp-server/dist/tools/youtube/schedule.d.ts +1 -1
  259. package/mcp-server/dist/tools/youtube/schedule.d.ts.map +1 -1
  260. package/mcp-server/dist/tools/youtube/schedule.js +1 -1
  261. package/mcp-server/dist/tools/youtube/schedule.js.map +1 -1
  262. package/mcp-server/dist/tools/youtube/source.d.ts +1 -1
  263. package/mcp-server/dist/tools/youtube/source.d.ts.map +1 -1
  264. package/mcp-server/dist/tools/youtube/source.js +1 -1
  265. package/mcp-server/dist/tools/youtube/source.js.map +1 -1
  266. package/mcp-server/dist/tools/youtube/upload.d.ts +1 -1
  267. package/mcp-server/dist/tools/youtube/upload.d.ts.map +1 -1
  268. package/mcp-server/dist/tools/youtube/upload.js +4 -9
  269. package/mcp-server/dist/tools/youtube/upload.js.map +1 -1
  270. package/mcp-server/dist/tui.js +18 -5
  271. package/mcp-server/dist/tui.js.map +1 -1
  272. package/mcp-server/dist/version.d.ts +5 -0
  273. package/mcp-server/dist/version.d.ts.map +1 -0
  274. package/mcp-server/dist/version.js +21 -0
  275. package/mcp-server/dist/version.js.map +1 -0
  276. package/mcp-server/package-lock.json +3 -3
  277. package/mcp-server/package.json +3 -3
  278. package/mcp-server/run.mjs +15 -9
  279. package/mcp-server/src/http.ts +18 -3
  280. package/mcp-server/src/index.ts +9 -6
  281. package/mcp-server/src/services/browser-bench.ts +1 -1
  282. package/mcp-server/src/services/budget-judgments.ts +183 -0
  283. package/mcp-server/src/services/categories.ts +77 -32
  284. package/mcp-server/src/services/codex-config-heal.ts +35 -4
  285. package/mcp-server/src/services/desktop-bench.ts +1 -1
  286. package/mcp-server/src/services/identity.ts +27 -0
  287. package/mcp-server/src/services/neural-interface.ts +34 -2
  288. package/mcp-server/src/services/typesafe-config.ts +4 -1
  289. package/mcp-server/src/tools/agents.ts +34 -15
  290. package/mcp-server/src/tools/bluesky.ts +1 -1
  291. package/mcp-server/src/tools/browser-advanced.ts +1 -1
  292. package/mcp-server/src/tools/browser-batch.ts +1 -1
  293. package/mcp-server/src/tools/browser-cheatsheet.ts +1 -1
  294. package/mcp-server/src/tools/browser-interact.ts +1 -1
  295. package/mcp-server/src/tools/browser-navigate.ts +1 -1
  296. package/mcp-server/src/tools/browser-observe.ts +1 -1
  297. package/mcp-server/src/tools/card-tools.ts +1 -1
  298. package/mcp-server/src/tools/category.ts +6 -1
  299. package/mcp-server/src/tools/choice.ts +1 -1
  300. package/mcp-server/src/tools/computer.ts +1 -1
  301. package/mcp-server/src/tools/discord-channel.ts +1 -1
  302. package/mcp-server/src/tools/discord-guild.ts +1 -1
  303. package/mcp-server/src/tools/discord-member.ts +1 -1
  304. package/mcp-server/src/tools/discord-message.ts +1 -1
  305. package/mcp-server/src/tools/discord-onboarding.ts +1 -1
  306. package/mcp-server/src/tools/discord-role.ts +1 -1
  307. package/mcp-server/src/tools/discord-thread.ts +1 -1
  308. package/mcp-server/src/tools/discord-webhook.ts +1 -1
  309. package/mcp-server/src/tools/fb-groups.ts +1 -1
  310. package/mcp-server/src/tools/forget.ts +1 -1
  311. package/mcp-server/src/tools/git-tools.ts +1 -1
  312. package/mcp-server/src/tools/gsc/experience.ts +1 -1
  313. package/mcp-server/src/tools/gsc/indexing.ts +1 -1
  314. package/mcp-server/src/tools/gsc/inspect.ts +1 -1
  315. package/mcp-server/src/tools/gsc/navigate.ts +1 -1
  316. package/mcp-server/src/tools/gsc/performance.ts +1 -1
  317. package/mcp-server/src/tools/gsc/settings.ts +1 -1
  318. package/mcp-server/src/tools/image-tools.ts +1 -1
  319. package/mcp-server/src/tools/leonardo-browser-tools.ts +1 -1
  320. package/mcp-server/src/tools/loop.ts +1 -1
  321. package/mcp-server/src/tools/memories.ts +1 -1
  322. package/mcp-server/src/tools/morelogin.ts +1 -1
  323. package/mcp-server/src/tools/profile.ts +1 -1
  324. package/mcp-server/src/tools/recall.ts +1 -1
  325. package/mcp-server/src/tools/reflect.ts +1 -1
  326. package/mcp-server/src/tools/remember.ts +13 -12
  327. package/mcp-server/src/tools/restore.ts +1 -1
  328. package/mcp-server/src/tools/style-guide-tools.ts +1 -1
  329. package/mcp-server/src/tools/sync.ts +1 -1
  330. package/mcp-server/src/tools/tictactoe-tools.ts +1 -1
  331. package/mcp-server/src/tools/utils.ts +1 -1
  332. package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
  333. package/mcp-server/src/tools/whiteboard-tools.ts +1 -1
  334. package/mcp-server/src/tools/youtube/analytics.ts +1 -1
  335. package/mcp-server/src/tools/youtube/download.ts +1 -1
  336. package/mcp-server/src/tools/youtube/media.ts +1 -1
  337. package/mcp-server/src/tools/youtube/metadata.ts +1 -1
  338. package/mcp-server/src/tools/youtube/navigate.ts +1 -1
  339. package/mcp-server/src/tools/youtube/playlist.ts +1 -1
  340. package/mcp-server/src/tools/youtube/schedule.ts +1 -1
  341. package/mcp-server/src/tools/youtube/source.ts +1 -1
  342. package/mcp-server/src/tools/youtube/upload.ts +4 -12
  343. package/mcp-server/src/tui.ts +24 -10
  344. package/mcp-server/src/version.ts +22 -0
  345. package/neural-interface/i18n/en.json +645 -57
  346. package/neural-interface/i18n/pt-BR.json +644 -56
  347. package/neural-interface/lib/account-usage.js +163 -0
  348. package/neural-interface/lib/agent-sandbox-elevation.js +102 -0
  349. package/neural-interface/lib/agent-sandbox-setting.js +57 -0
  350. package/neural-interface/lib/assistant-api.js +274 -52
  351. package/neural-interface/lib/assistant-brains/claude.js +15 -2
  352. package/neural-interface/lib/assistant-brains/codex.js +3 -0
  353. package/neural-interface/lib/assistant-brains/opencode.js +17 -4
  354. package/neural-interface/lib/assistant-budget-planner.js +390 -0
  355. package/neural-interface/lib/assistant-budget-split.js +408 -0
  356. package/neural-interface/lib/assistant-budget.js +94 -95
  357. package/neural-interface/lib/assistant-config.js +62 -14
  358. package/neural-interface/lib/assistant-dispatch.js +432 -125
  359. package/neural-interface/lib/assistant-jev.js +26 -3
  360. package/neural-interface/lib/assistant-persona.js +68 -11
  361. package/neural-interface/lib/assistant-plan-permissions.js +8 -1
  362. package/neural-interface/lib/assistant-route-accounts.js +14 -0
  363. package/neural-interface/lib/assistant-route-gate-codex.js +3 -1
  364. package/neural-interface/lib/assistant-route-gate.js +19 -3
  365. package/neural-interface/lib/assistant-router.js +373 -42
  366. package/neural-interface/lib/assistant-runtime.js +312 -48
  367. package/neural-interface/lib/assistant-user-writer.js +4 -2
  368. package/neural-interface/lib/augmented-path.js +317 -53
  369. package/neural-interface/lib/claude-accounts.js +342 -31
  370. package/neural-interface/lib/claude-agent-bridge.js +90 -3
  371. package/neural-interface/lib/claude-executable.js +6 -1
  372. package/neural-interface/lib/claude-model-catalog.js +3 -3
  373. package/neural-interface/lib/claude-panel-auth.js +221 -0
  374. package/neural-interface/lib/claude-profile-api-key.js +62 -0
  375. package/neural-interface/lib/cli-install-api.js +137 -0
  376. package/neural-interface/lib/cli-install.js +797 -0
  377. package/neural-interface/lib/cli-path.js +130 -0
  378. package/neural-interface/lib/cli-resolver.js +744 -0
  379. package/neural-interface/lib/cli-update-planner.js +143 -7
  380. package/neural-interface/lib/codex-account-auth.js +180 -0
  381. package/neural-interface/lib/codex-accounts.js +502 -0
  382. package/neural-interface/lib/codex-capabilities.js +2 -1
  383. package/neural-interface/lib/codex-compat-rpc.js +8 -2
  384. package/neural-interface/lib/codex-mcp-approvals.js +5 -1
  385. package/neural-interface/lib/codex-panel-auth.js +62 -0
  386. package/neural-interface/lib/codex-session-settings.js +1 -1
  387. package/neural-interface/lib/codex-settings-schema.js +122 -0
  388. package/neural-interface/lib/desktop/config.js +3 -0
  389. package/neural-interface/lib/exec-loop-command.js +3 -1
  390. package/neural-interface/lib/git-bash/bzip2.js +339 -0
  391. package/neural-interface/lib/git-bash/claude-settings.js +73 -0
  392. package/neural-interface/lib/git-bash/extract-worker.js +34 -0
  393. package/neural-interface/lib/git-bash/install.js +529 -0
  394. package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
  395. package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
  396. package/neural-interface/lib/git-bash/manager.js +268 -0
  397. package/neural-interface/lib/git-bash/pins.js +206 -0
  398. package/neural-interface/lib/git-bash/pins.json +34 -0
  399. package/neural-interface/lib/git-bash/resolver.js +194 -0
  400. package/neural-interface/lib/git-read.js +34 -2
  401. package/neural-interface/lib/install-details.js +163 -0
  402. package/neural-interface/lib/mcp-installer.js +14 -2
  403. package/neural-interface/lib/native-loop-providers.js +22 -4
  404. package/neural-interface/lib/native-loop-runtime.js +9 -4
  405. package/neural-interface/lib/os-path.js +319 -0
  406. package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
  407. package/neural-interface/lib/provider-panel-terminal-auth.js +96 -0
  408. package/neural-interface/lib/pty-host/core.js +2 -1
  409. package/neural-interface/lib/pty-host/manager.js +27 -2
  410. package/neural-interface/lib/remote-policy.js +20 -11
  411. package/neural-interface/lib/session-indexer.js +3 -1
  412. package/neural-interface/lib/session-title-generator.js +6 -3
  413. package/neural-interface/lib/system-about.js +340 -0
  414. package/neural-interface/lib/system-activity.js +66 -0
  415. package/neural-interface/lib/terminal-env.js +11 -3
  416. package/neural-interface/lib/terminal-path.js +55 -0
  417. package/neural-interface/lib/virtual-socket.js +12 -1
  418. package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
  419. package/neural-interface/lib/whatsapp/bridge.js +78 -34
  420. package/neural-interface/lib/whatsapp/commands.js +13 -14
  421. package/neural-interface/lib/whatsapp/config.js +134 -20
  422. package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
  423. package/neural-interface/lib/whatsapp/host-core.js +168 -35
  424. package/neural-interface/lib/whatsapp/inbound.js +5 -3
  425. package/neural-interface/lib/whatsapp/local-file.js +524 -0
  426. package/neural-interface/lib/whatsapp/manager.js +92 -32
  427. package/neural-interface/lib/whatsapp/paths.js +11 -1
  428. package/neural-interface/lib/whatsapp/protocol.js +65 -5
  429. package/neural-interface/lib/whatsapp/redact.js +63 -0
  430. package/neural-interface/lib/whatsapp/service.js +343 -91
  431. package/neural-interface/lib/whatsapp/transcription.js +463 -0
  432. package/neural-interface/package-lock.json +2 -2
  433. package/neural-interface/package.json +1 -1
  434. package/neural-interface/public/index.html +2 -0
  435. package/neural-interface/public/index2d.html +2 -0
  436. package/neural-interface/public/offline.html +16 -1
  437. package/neural-interface/public/onboarding.html +96 -2
  438. package/neural-interface/public/shared/account-i18n.js +7 -0
  439. package/neural-interface/public/shared/account-menu.js +542 -0
  440. package/neural-interface/public/shared/account-usage.js +79 -0
  441. package/neural-interface/public/shared/account-view.js +336 -0
  442. package/neural-interface/public/shared/agent-runtime-options.js +28 -8
  443. package/neural-interface/public/shared/assistant/asst-account-menu.js +298 -0
  444. package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
  445. package/neural-interface/public/shared/assistant/asst-api.js +39 -2
  446. package/neural-interface/public/shared/assistant/asst-brain-picker.js +288 -32
  447. package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
  448. package/neural-interface/public/shared/assistant/asst-mascot.js +126 -7
  449. package/neural-interface/public/shared/assistant/asst-menu.js +219 -80
  450. package/neural-interface/public/shared/assistant/asst-model-chip.js +135 -0
  451. package/neural-interface/public/shared/assistant/asst-panel.js +87 -16
  452. package/neural-interface/public/shared/assistant/asst-render.js +38 -7
  453. package/neural-interface/public/shared/assistant/asst-route.js +72 -14
  454. package/neural-interface/public/shared/assistant/asst-routes-editor.js +45 -6
  455. package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
  456. package/neural-interface/public/shared/assistant/asst-state.js +1 -1
  457. package/neural-interface/public/shared/assistant/asst-styles.js +160 -29
  458. package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
  459. package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
  460. package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
  461. package/neural-interface/public/shared/cdx/cdx-panel.js +49 -108
  462. package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
  463. package/neural-interface/public/shared/cdx/cdx-styles.js +172 -111
  464. package/neural-interface/public/shared/cdx/cdx-tabs.js +264 -664
  465. package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
  466. package/neural-interface/public/shared/claude-account-view.js +3 -0
  467. package/neural-interface/public/shared/cli-install-view.js +392 -0
  468. package/neural-interface/public/shared/cli-status.js +282 -22
  469. package/neural-interface/public/shared/cp/cp-events.js +2 -1
  470. package/neural-interface/public/shared/cp/cp-styles.js +46 -0
  471. package/neural-interface/public/shared/desktop-bridge.js +39 -0
  472. package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
  473. package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
  474. package/neural-interface/public/shared/desktop-titlebar.js +153 -0
  475. package/neural-interface/public/shared/desktop-update-view.js +462 -0
  476. package/neural-interface/public/shared/editor/editor-panel.js +1 -1
  477. package/neural-interface/public/shared/explorer/fx-main.js +1 -1
  478. package/neural-interface/public/shared/git-bash-view.js +145 -0
  479. package/neural-interface/public/shared/html-shell.js +1 -0
  480. package/neural-interface/public/shared/i18n.js +19 -1
  481. package/neural-interface/public/shared/install-details-view.js +86 -0
  482. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -1
  483. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +36 -1
  484. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +17 -0
  485. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +169 -2
  486. package/neural-interface/public/shared/pwa-install-page.js +3 -1
  487. package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
  488. package/neural-interface/public/shared/settings/settings-about.js +254 -0
  489. package/neural-interface/public/shared/settings/settings-ia.js +15 -0
  490. package/neural-interface/public/shared/showcase-privacy.js +490 -0
  491. package/neural-interface/public/shared/storage.js +30 -3
  492. package/neural-interface/public/shared/styles.css +281 -32
  493. package/neural-interface/public/shared/synabun-mascot.js +48 -2
  494. package/neural-interface/public/shared/ui-claude-panel.js +262 -217
  495. package/neural-interface/public/shared/ui-cli-banner.js +355 -0
  496. package/neural-interface/public/shared/ui-explorer.js +2 -0
  497. package/neural-interface/public/shared/ui-git-bash.js +124 -0
  498. package/neural-interface/public/shared/ui-judgments.js +10 -1
  499. package/neural-interface/public/shared/ui-loading.js +30 -6
  500. package/neural-interface/public/shared/ui-menubar.js +13 -3
  501. package/neural-interface/public/shared/ui-navbar.js +3 -1
  502. package/neural-interface/public/shared/ui-provider-auth.js +104 -0
  503. package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
  504. package/neural-interface/public/shared/ui-sessions.js +9 -3
  505. package/neural-interface/public/shared/ui-settings.js +55 -3
  506. package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
  507. package/neural-interface/public/shared/ui-sync.js +8 -0
  508. package/neural-interface/public/shared/ui-terminal.js +17 -0
  509. package/neural-interface/public/shared/ui-tool-updates.js +5 -0
  510. package/neural-interface/public/shared/ui-update.js +598 -7
  511. package/neural-interface/public/shared/ui-whatsapp.js +21 -6
  512. package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
  513. package/neural-interface/public/shared/worker-end.js +2 -0
  514. package/neural-interface/public/sw.js +1 -1
  515. package/neural-interface/public/variant/2d/main.js +10 -1
  516. package/neural-interface/public/variant/3d/main.js +10 -1
  517. package/neural-interface/scripts/rebuild-pty.js +14 -2
  518. package/neural-interface/server.js +1384 -679
  519. package/npm-shrinkwrap.json +4424 -0
  520. package/package.json +57 -2
  521. package/postinstall.js +61 -47
  522. package/preuninstall.js +8 -35
  523. package/setup.js +80 -47
  524. package/skills/synabun/modules/idea.md +1 -1
  525. package/skills/synabun/modules/schedule.md +1 -1
  526. package/updater.cmd +2 -2
  527. 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);
@@ -288,14 +388,27 @@ export function openMenu(anchor, opts = {}) {
288
388
  closeMenu({ restoreFocus: !!item.restoreFocus });
289
389
  item.onSelect?.(item, node);
290
390
  };
291
- buildItems(list, items, { role, onActivate: activate });
292
- if (width) menu.style.minWidth = opts.repositionOnResize ? `min(${width}px, calc(100vw - 16px))` : `${width}px`;
293
- if (width && opts.repositionOnResize) menu.style.width = `min(${width}px, calc(100vw - 16px))`;
391
+ if (opts.content) list.appendChild(opts.content);
392
+ else buildItems(list, items, { role, onActivate: activate });
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;
294
406
  document.body.appendChild(menu);
295
- positionMenu(menu, anchor, placement);
407
+ const place = () => { positionMenu(menu, anchor, { placement, bounds: opts.bounds || null, width }); titleCutRows(menu); };
408
+ place();
296
409
 
297
410
  if (filterInput) {
298
- filterInput.addEventListener('input', () => { applyMenuFilter(list, filterInput.value, opts.filterEmptyLabel); positionMenu(menu, anchor, placement); });
411
+ filterInput.addEventListener('input', () => { applyMenuFilter(list, filterInput.value, opts.filterEmptyLabel); place(); });
299
412
  }
300
413
 
301
414
  const onDocDown = (e) => {
@@ -310,7 +423,21 @@ export function openMenu(anchor, opts = {}) {
310
423
  const inInput = target?.classList?.contains('asst-dd-input');
311
424
  const nodes = focusables(menu);
312
425
  const idx = nodes.indexOf(document.activeElement);
313
- 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
+ }
432
+ if (role === 'dialog') {
433
+ if (e.key === 'Tab') {
434
+ const fields = [...menu.querySelectorAll('button, input')].filter(node => !node.disabled && !node.hidden);
435
+ const index = fields.indexOf(document.activeElement);
436
+ if (e.shiftKey && index <= 0) { e.preventDefault(); fields.at(-1)?.focus(); }
437
+ else if (!e.shiftKey && (index < 0 || index === fields.length - 1)) { e.preventDefault(); fields[0]?.focus(); }
438
+ }
439
+ return; // native number/switch keys and Enter belong to the form
440
+ }
314
441
  if (e.key === 'Tab') { closeMenu(); return; }
315
442
  if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
316
443
  e.preventDefault(); e.stopPropagation();
@@ -344,15 +471,16 @@ export function openMenu(anchor, opts = {}) {
344
471
  // Keep single-letter global keybinds out of an open menu.
345
472
  e.stopPropagation();
346
473
  };
347
- const onResize = () => opts.repositionOnResize ? positionMenu(menu, anchor, placement) : closeMenu();
474
+ const onResize = () => (opts.repositionOnResize || opts.bounds ? place() : closeMenu());
348
475
  const onBlur = () => closeMenu();
349
476
  document.addEventListener('mousedown', onDocDown, true);
350
- document.addEventListener('keydown', onKey, true);
477
+ const keyTarget = role === 'dialog' ? window : document;
478
+ keyTarget.addEventListener('keydown', onKey, true);
351
479
  window.addEventListener('resize', onResize);
352
480
  window.addEventListener('blur', onBlur);
353
481
  const cleanup = () => {
354
482
  document.removeEventListener('mousedown', onDocDown, true);
355
- document.removeEventListener('keydown', onKey, true);
483
+ keyTarget.removeEventListener('keydown', onKey, true);
356
484
  window.removeEventListener('resize', onResize);
357
485
  window.removeEventListener('blur', onBlur);
358
486
  };
@@ -360,6 +488,7 @@ export function openMenu(anchor, opts = {}) {
360
488
  _open = { menu, anchor, onClose, cleanup };
361
489
  if (anchor.getAttribute('aria-haspopup')) anchor.setAttribute('aria-expanded', 'true');
362
490
  anchor.setAttribute('aria-controls', menu.id);
491
+ opts.onOpen?.(menu);
363
492
 
364
493
  const selected = list.querySelector('.asst-dd-item[data-checked="1"]:not([disabled])');
365
494
  const first = focusables(menu).find(n => n.classList.contains('asst-dd-item')) || focusables(menu)[0];
@@ -369,7 +498,17 @@ export function openMenu(anchor, opts = {}) {
369
498
  } else {
370
499
  setFocused(menu, selected || first || null);
371
500
  }
372
- 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 };
373
512
  }
374
513
 
375
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
+ }