synabun 2.0.0 → 2.0.5

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 (334) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/NOTICE +5 -0
  3. package/README.md +1 -1
  4. package/THIRD-PARTY-LICENSES.md +46 -0
  5. package/benchmarks/fixtures/browser/README.md +1 -1
  6. package/benchmarks/fixtures/desktop/README.md +1 -1
  7. package/docs/whatsapp.md +2 -2
  8. package/hooks/claude-code/pre-compact.mjs +11 -0
  9. package/hooks/claude-code/prompt-submit.mjs +60 -184
  10. package/hooks/claude-code/session-start.mjs +73 -16
  11. package/hooks/claude-code/shared.mjs +20 -0
  12. package/hooks/claude-code/stop.mjs +5 -5
  13. package/lib/start-launcher.js +88 -1
  14. package/mcp-server/package-lock.json +2 -2
  15. package/mcp-server/package.json +1 -3
  16. package/neural-interface/i18n/en.json +1500 -153
  17. package/neural-interface/i18n/pt-BR.json +1498 -152
  18. package/neural-interface/lib/assistant-api.js +62 -6
  19. package/neural-interface/lib/assistant-brains/claude.js +4 -0
  20. package/neural-interface/lib/assistant-catalog.js +179 -27
  21. package/neural-interface/lib/assistant-config.js +31 -2
  22. package/neural-interface/lib/assistant-dispatch.js +62 -13
  23. package/neural-interface/lib/assistant-hidden-models.js +393 -66
  24. package/neural-interface/lib/assistant-memory.js +24 -10
  25. package/neural-interface/lib/assistant-persona.js +6 -2
  26. package/neural-interface/lib/assistant-pricing.js +21 -6
  27. package/neural-interface/lib/assistant-runtime.js +181 -12
  28. package/neural-interface/lib/assistant-task-prompt.js +13 -3
  29. package/neural-interface/lib/assistant-test-density.js +142 -0
  30. package/neural-interface/lib/assistant-user-context-budgets.js +123 -0
  31. package/neural-interface/lib/assistant-user-context.js +908 -0
  32. package/neural-interface/lib/assistant-user-knowledge.js +328 -0
  33. package/neural-interface/lib/assistant-user-learn.js +404 -0
  34. package/neural-interface/lib/assistant-user-threads.js +226 -0
  35. package/neural-interface/lib/assistant-user-writer.js +181 -0
  36. package/neural-interface/lib/browser-background-tab.js +134 -0
  37. package/neural-interface/lib/browser-screenshot.js +678 -61
  38. package/neural-interface/lib/claude-agent-bridge.js +33 -6
  39. package/neural-interface/lib/claude-model-catalog.js +30 -2
  40. package/neural-interface/lib/claude-temporary.js +46 -7
  41. package/neural-interface/lib/cloudflared/install.js +533 -0
  42. package/neural-interface/lib/cloudflared/manager.js +545 -0
  43. package/neural-interface/lib/cloudflared/pins.js +115 -0
  44. package/neural-interface/lib/cloudflared/pins.json +63 -0
  45. package/neural-interface/lib/cloudflared/tunnel.js +265 -0
  46. package/neural-interface/lib/editor-completion-endpoint.js +264 -0
  47. package/neural-interface/lib/editor-completion-opencode.js +436 -0
  48. package/neural-interface/lib/editor-completion.js +714 -0
  49. package/neural-interface/lib/editor-config.js +149 -0
  50. package/neural-interface/lib/editor-files-api.js +268 -0
  51. package/neural-interface/lib/editor-git-api.js +134 -0
  52. package/neural-interface/lib/file-explorer-api.js +1039 -0
  53. package/neural-interface/lib/file-explorer-system.js +355 -0
  54. package/neural-interface/lib/file-explorer-trash.ps1 +85 -0
  55. package/neural-interface/lib/file-explorer-watch.js +239 -0
  56. package/neural-interface/lib/git-diff-api.js +103 -0
  57. package/neural-interface/lib/git-read.js +202 -0
  58. package/neural-interface/lib/greeting/api.js +89 -0
  59. package/neural-interface/lib/greeting/budgets.js +90 -0
  60. package/neural-interface/lib/greeting/engine.js +461 -0
  61. package/neural-interface/lib/greeting/facts.js +320 -0
  62. package/neural-interface/lib/greeting/sources.js +252 -0
  63. package/neural-interface/lib/greeting/writer.js +139 -0
  64. package/neural-interface/lib/http-guards.js +189 -13
  65. package/neural-interface/lib/machine-names.js +88 -0
  66. package/neural-interface/lib/memory-api.js +31 -7
  67. package/neural-interface/lib/native-loop-providers.js +10 -5
  68. package/neural-interface/lib/plugin-installer.js +4 -2
  69. package/neural-interface/lib/project-storage.js +5 -7
  70. package/neural-interface/lib/request-gate.js +589 -0
  71. package/neural-interface/lib/ui-blob-api.js +108 -0
  72. package/neural-interface/lib/ui-blob-store.js +68 -0
  73. package/neural-interface/package-lock.json +701 -2
  74. package/neural-interface/package.json +13 -2
  75. package/neural-interface/public/install-app.html +9 -6
  76. package/neural-interface/public/offline.html +3 -2
  77. package/neural-interface/public/onboarding.html +12 -1
  78. package/neural-interface/public/shared/agent-runtime-options.js +22 -6
  79. package/neural-interface/public/shared/assistant/asst-automation.js +286 -0
  80. package/neural-interface/public/shared/assistant/asst-brain-picker.js +312 -62
  81. package/neural-interface/public/shared/assistant/asst-composer.js +11 -1
  82. package/neural-interface/public/shared/assistant/asst-mascot.js +18 -58
  83. package/neural-interface/public/shared/assistant/asst-menu.js +12 -8
  84. package/neural-interface/public/shared/assistant/asst-panel.js +150 -28
  85. package/neural-interface/public/shared/assistant/asst-render.js +162 -19
  86. package/neural-interface/public/shared/assistant/asst-route-models.js +108 -0
  87. package/neural-interface/public/shared/assistant/asst-route.js +23 -329
  88. package/neural-interface/public/shared/assistant/asst-routes-editor.js +495 -0
  89. package/neural-interface/public/shared/assistant/asst-sanitize.js +8 -1
  90. package/neural-interface/public/shared/assistant/asst-sidepanel.js +31 -9
  91. package/neural-interface/public/shared/assistant/asst-state.js +185 -1
  92. package/neural-interface/public/shared/assistant/asst-styles.js +158 -4
  93. package/neural-interface/public/shared/blob-store.js +111 -22
  94. package/neural-interface/public/shared/cdx/cdx-panel.js +32 -11
  95. package/neural-interface/public/shared/cdx/cdx-render.js +25 -89
  96. package/neural-interface/public/shared/cdx/cdx-styles.js +36 -0
  97. package/neural-interface/public/shared/cdx/cdx-tabs.js +87 -4
  98. package/neural-interface/public/shared/constants.js +6 -1
  99. package/neural-interface/public/shared/cp/cp-rehydrate.js +2 -1
  100. package/neural-interface/public/shared/cp/cp-styles.js +15 -0
  101. package/neural-interface/public/shared/editor/editor-ai-logic.js +409 -0
  102. package/neural-interface/public/shared/editor/editor-ai.js +619 -0
  103. package/neural-interface/public/shared/editor/editor-commands.js +212 -0
  104. package/neural-interface/public/shared/editor/editor-compare.js +103 -0
  105. package/neural-interface/public/shared/editor/editor-completion-list.js +172 -0
  106. package/neural-interface/public/shared/editor/editor-doc.js +257 -0
  107. package/neural-interface/public/shared/editor/editor-git-logic.js +131 -0
  108. package/neural-interface/public/shared/editor/editor-git.js +95 -0
  109. package/neural-interface/public/shared/editor/editor-languages.js +156 -0
  110. package/neural-interface/public/shared/editor/editor-lint.js +229 -0
  111. package/neural-interface/public/shared/editor/editor-loader.js +51 -0
  112. package/neural-interface/public/shared/editor/editor-markdown.js +90 -0
  113. package/neural-interface/public/shared/editor/editor-menu.js +233 -0
  114. package/neural-interface/public/shared/editor/editor-minimap.js +371 -0
  115. package/neural-interface/public/shared/editor/editor-palette-logic.js +192 -0
  116. package/neural-interface/public/shared/editor/editor-palette.js +125 -0
  117. package/neural-interface/public/shared/editor/editor-panel-logic.js +288 -0
  118. package/neural-interface/public/shared/editor/editor-panel.js +2099 -0
  119. package/neural-interface/public/shared/editor/editor-prefs.js +224 -0
  120. package/neural-interface/public/shared/editor/editor-safety.js +500 -0
  121. package/neural-interface/public/shared/editor/editor-search.js +352 -0
  122. package/neural-interface/public/shared/editor/editor-statusbar.js +144 -0
  123. package/neural-interface/public/shared/editor/editor-tabs-logic.js +331 -0
  124. package/neural-interface/public/shared/editor/editor-tabs.js +233 -0
  125. package/neural-interface/public/shared/editor/editor-theme.js +244 -0
  126. package/neural-interface/public/shared/editor/editor-view.js +332 -0
  127. package/neural-interface/public/shared/explorer/fx-api.js +216 -0
  128. package/neural-interface/public/shared/explorer/fx-links.js +204 -0
  129. package/neural-interface/public/shared/explorer/fx-logic.js +537 -0
  130. package/neural-interface/public/shared/explorer/fx-main.js +865 -0
  131. package/neural-interface/public/shared/explorer/fx-names.js +87 -0
  132. package/neural-interface/public/shared/explorer/fx-ops.js +370 -0
  133. package/neural-interface/public/shared/explorer/fx-popups.js +490 -0
  134. package/neural-interface/public/shared/explorer/fx-tree.js +790 -0
  135. package/neural-interface/public/shared/file-links.js +339 -0
  136. package/neural-interface/public/shared/greeting/greeting-card.js +178 -0
  137. package/neural-interface/public/shared/greeting/greeting-view.js +148 -0
  138. package/neural-interface/public/shared/html-shell.js +160 -165
  139. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -0
  140. package/neural-interface/public/shared/ocp-v2/ocp-v2-greeting.js +47 -0
  141. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +99 -17
  142. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
  143. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +5 -0
  144. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +40 -0
  145. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +3 -2
  146. package/neural-interface/public/shared/pwa-install-page.js +29 -1
  147. package/neural-interface/public/shared/pwa-install.css +3 -3
  148. package/neural-interface/public/shared/pwa-install.js +100 -1
  149. package/neural-interface/public/shared/settings/settings-code-editor-pane.js +412 -0
  150. package/neural-interface/public/shared/settings/settings-code-editor.js +328 -0
  151. package/neural-interface/public/shared/settings/settings-greeting.js +133 -0
  152. package/neural-interface/public/shared/settings/settings-ia.js +18 -4
  153. package/neural-interface/public/shared/settings/settings-user-context.js +154 -0
  154. package/neural-interface/public/shared/start-bridge.js +238 -19
  155. package/neural-interface/public/shared/styles.css +1414 -721
  156. package/neural-interface/public/shared/synabun-mascot.js +20 -56
  157. package/neural-interface/public/shared/tunnel-view.js +167 -0
  158. package/neural-interface/public/shared/ui-claude-panel.js +157 -16
  159. package/neural-interface/public/shared/ui-detail.js +15 -0
  160. package/neural-interface/public/shared/ui-explorer.js +22 -0
  161. package/neural-interface/public/shared/ui-file-explorer.js +481 -2496
  162. package/neural-interface/public/shared/ui-file-links.js +103 -0
  163. package/neural-interface/public/shared/ui-help.js +26 -0
  164. package/neural-interface/public/shared/ui-invite.js +126 -116
  165. package/neural-interface/public/shared/ui-keybinds.js +4 -0
  166. package/neural-interface/public/shared/ui-loading.js +159 -15
  167. package/neural-interface/public/shared/ui-menubar.js +12 -30
  168. package/neural-interface/public/shared/ui-native-loop-router-state.js +11 -0
  169. package/neural-interface/public/shared/ui-native-loop-router.js +15 -2
  170. package/neural-interface/public/shared/ui-navbar.js +45 -21
  171. package/neural-interface/public/shared/ui-settings.js +455 -488
  172. package/neural-interface/public/shared/ui-sidepanel-runtime.js +2 -1
  173. package/neural-interface/public/shared/ui-sync.js +16 -0
  174. package/neural-interface/public/shared/ui-terminal.js +23 -5
  175. package/neural-interface/public/shared/ui-tunnel.js +130 -0
  176. package/neural-interface/public/shared/ui-tutorial-steps.js +6 -2
  177. package/neural-interface/public/shared/ui-whiteboard.js +12 -0
  178. package/neural-interface/public/shared/ui-worker-end.js +245 -0
  179. package/neural-interface/public/shared/ui-workspaces.js +41 -27
  180. package/neural-interface/public/shared/worker-end.js +251 -0
  181. package/neural-interface/public/sw.js +2 -2
  182. package/neural-interface/public/vendor/codemirror/chunks/apl-CSDKT72Z.js +1 -0
  183. package/neural-interface/public/vendor/codemirror/chunks/asciiarmor-JYYPU5BM.js +1 -0
  184. package/neural-interface/public/vendor/codemirror/chunks/asn1-NRWCZDA6.js +1 -0
  185. package/neural-interface/public/vendor/codemirror/chunks/asterisk-5OU6D4ZQ.js +1 -0
  186. package/neural-interface/public/vendor/codemirror/chunks/brainfuck-ID4N4SYC.js +1 -0
  187. package/neural-interface/public/vendor/codemirror/chunks/chunk-4RZWGCS2.js +1 -0
  188. package/neural-interface/public/vendor/codemirror/chunks/chunk-ACTJM3JR.js +11 -0
  189. package/neural-interface/public/vendor/codemirror/chunks/chunk-S6BMAXLK.js +1 -0
  190. package/neural-interface/public/vendor/codemirror/chunks/chunk-TO2RDOHQ.js +3 -0
  191. package/neural-interface/public/vendor/codemirror/chunks/chunk-US2GMZZQ.js +13 -0
  192. package/neural-interface/public/vendor/codemirror/chunks/chunk-WYYO5ZAJ.js +1 -0
  193. package/neural-interface/public/vendor/codemirror/chunks/chunk-XALZTDJC.js +1 -0
  194. package/neural-interface/public/vendor/codemirror/chunks/chunk-YRNUTRDV.js +1 -0
  195. package/neural-interface/public/vendor/codemirror/chunks/chunk-Z3QOKH62.js +1 -0
  196. package/neural-interface/public/vendor/codemirror/chunks/clike-OWQBSOGL.js +1 -0
  197. package/neural-interface/public/vendor/codemirror/chunks/clojure-KV75UQWC.js +1 -0
  198. package/neural-interface/public/vendor/codemirror/chunks/cmake-GTEP5Y3C.js +1 -0
  199. package/neural-interface/public/vendor/codemirror/chunks/cobol-WCZ7QEGJ.js +1 -0
  200. package/neural-interface/public/vendor/codemirror/chunks/coffeescript-TGLBOT6B.js +1 -0
  201. package/neural-interface/public/vendor/codemirror/chunks/commonlisp-WVVI432V.js +1 -0
  202. package/neural-interface/public/vendor/codemirror/chunks/crystal-S6R6HLKQ.js +1 -0
  203. package/neural-interface/public/vendor/codemirror/chunks/css-PDA2RINP.js +1 -0
  204. package/neural-interface/public/vendor/codemirror/chunks/cypher-HL5QPL4C.js +1 -0
  205. package/neural-interface/public/vendor/codemirror/chunks/d-HHJH2HAJ.js +1 -0
  206. package/neural-interface/public/vendor/codemirror/chunks/diff-MN3A35BY.js +1 -0
  207. package/neural-interface/public/vendor/codemirror/chunks/dist-25SQLBVS.js +3 -0
  208. package/neural-interface/public/vendor/codemirror/chunks/dist-27ZGGJJS.js +1 -0
  209. package/neural-interface/public/vendor/codemirror/chunks/dist-4E52OJIF.js +1 -0
  210. package/neural-interface/public/vendor/codemirror/chunks/dist-6FT73XDC.js +2 -0
  211. package/neural-interface/public/vendor/codemirror/chunks/dist-BE6YGRMW.js +1 -0
  212. package/neural-interface/public/vendor/codemirror/chunks/dist-CJSGKW2O.js +1 -0
  213. package/neural-interface/public/vendor/codemirror/chunks/dist-CRPH2WLI.js +1 -0
  214. package/neural-interface/public/vendor/codemirror/chunks/dist-CTG7JEQN.js +1 -0
  215. package/neural-interface/public/vendor/codemirror/chunks/dist-I3FEVELX.js +1 -0
  216. package/neural-interface/public/vendor/codemirror/chunks/dist-IM5BBSWZ.js +1 -0
  217. package/neural-interface/public/vendor/codemirror/chunks/dist-KERUKTIF.js +1 -0
  218. package/neural-interface/public/vendor/codemirror/chunks/dist-LXTMWZAM.js +1 -0
  219. package/neural-interface/public/vendor/codemirror/chunks/dist-MABASSET.js +1 -0
  220. package/neural-interface/public/vendor/codemirror/chunks/dist-MJVBUZLL.js +3 -0
  221. package/neural-interface/public/vendor/codemirror/chunks/dist-MNB3WZVU.js +1 -0
  222. package/neural-interface/public/vendor/codemirror/chunks/dist-QYWXOE3U.js +7 -0
  223. package/neural-interface/public/vendor/codemirror/chunks/dist-TEBOFWLZ.js +1 -0
  224. package/neural-interface/public/vendor/codemirror/chunks/dist-UNXFROM3.js +1 -0
  225. package/neural-interface/public/vendor/codemirror/chunks/dist-XL2SLQO6.js +1 -0
  226. package/neural-interface/public/vendor/codemirror/chunks/dist-XO42Y3AP.js +6 -0
  227. package/neural-interface/public/vendor/codemirror/chunks/dist-Y2WWDWJK.js +1 -0
  228. package/neural-interface/public/vendor/codemirror/chunks/dist-YFEZKWMU.js +1 -0
  229. package/neural-interface/public/vendor/codemirror/chunks/dist-ZY7SWQO6.js +1 -0
  230. package/neural-interface/public/vendor/codemirror/chunks/dockerfile-3Y4R4NCH.js +1 -0
  231. package/neural-interface/public/vendor/codemirror/chunks/dtd-5343GJ4P.js +1 -0
  232. package/neural-interface/public/vendor/codemirror/chunks/dylan-FI2CXSW2.js +1 -0
  233. package/neural-interface/public/vendor/codemirror/chunks/ebnf-GNQ77YWA.js +1 -0
  234. package/neural-interface/public/vendor/codemirror/chunks/ecl-MRPUAE2L.js +1 -0
  235. package/neural-interface/public/vendor/codemirror/chunks/eiffel-DZROES27.js +1 -0
  236. package/neural-interface/public/vendor/codemirror/chunks/elm-4KICRFKQ.js +1 -0
  237. package/neural-interface/public/vendor/codemirror/chunks/erlang-3IHYC263.js +1 -0
  238. package/neural-interface/public/vendor/codemirror/chunks/factor-C6D3E5UK.js +1 -0
  239. package/neural-interface/public/vendor/codemirror/chunks/fcl-VYTWUJMI.js +1 -0
  240. package/neural-interface/public/vendor/codemirror/chunks/forth-ZBO2EH5M.js +1 -0
  241. package/neural-interface/public/vendor/codemirror/chunks/fortran-5NDCRWIK.js +1 -0
  242. package/neural-interface/public/vendor/codemirror/chunks/gas-SKRMOXFP.js +1 -0
  243. package/neural-interface/public/vendor/codemirror/chunks/gherkin-L45YXZLW.js +1 -0
  244. package/neural-interface/public/vendor/codemirror/chunks/groovy-GLPTLN44.js +1 -0
  245. package/neural-interface/public/vendor/codemirror/chunks/haskell-E7NF7P4Y.js +1 -0
  246. package/neural-interface/public/vendor/codemirror/chunks/haxe-K2NVDOB6.js +1 -0
  247. package/neural-interface/public/vendor/codemirror/chunks/http-Q6DRQIJB.js +1 -0
  248. package/neural-interface/public/vendor/codemirror/chunks/idl-MZYNJVLN.js +1 -0
  249. package/neural-interface/public/vendor/codemirror/chunks/javascript-6YRYK2HR.js +1 -0
  250. package/neural-interface/public/vendor/codemirror/chunks/julia-HXEL2T5G.js +1 -0
  251. package/neural-interface/public/vendor/codemirror/chunks/livescript-MV7CI2JZ.js +1 -0
  252. package/neural-interface/public/vendor/codemirror/chunks/lua-E36NHOOD.js +1 -0
  253. package/neural-interface/public/vendor/codemirror/chunks/mathematica-3XOZZCMC.js +1 -0
  254. package/neural-interface/public/vendor/codemirror/chunks/mbox-ODYGFKS7.js +1 -0
  255. package/neural-interface/public/vendor/codemirror/chunks/mirc-WB43AWF5.js +1 -0
  256. package/neural-interface/public/vendor/codemirror/chunks/mllike-ZT6ET7ZH.js +1 -0
  257. package/neural-interface/public/vendor/codemirror/chunks/modelica-LLMZPFGA.js +1 -0
  258. package/neural-interface/public/vendor/codemirror/chunks/mscgen-2OPNNYFH.js +1 -0
  259. package/neural-interface/public/vendor/codemirror/chunks/mumps-6P3HWJH6.js +1 -0
  260. package/neural-interface/public/vendor/codemirror/chunks/nginx-TCGKQTRW.js +1 -0
  261. package/neural-interface/public/vendor/codemirror/chunks/nsis-ATIGIXTJ.js +1 -0
  262. package/neural-interface/public/vendor/codemirror/chunks/ntriples-5PLBI5OV.js +1 -0
  263. package/neural-interface/public/vendor/codemirror/chunks/octave-QF2S3NL6.js +1 -0
  264. package/neural-interface/public/vendor/codemirror/chunks/oz-JFIVFFH2.js +1 -0
  265. package/neural-interface/public/vendor/codemirror/chunks/pascal-JG6SW76V.js +1 -0
  266. package/neural-interface/public/vendor/codemirror/chunks/perl-4B3IWVVO.js +1 -0
  267. package/neural-interface/public/vendor/codemirror/chunks/pig-ABEBH4MI.js +1 -0
  268. package/neural-interface/public/vendor/codemirror/chunks/powershell-H32XE7TL.js +1 -0
  269. package/neural-interface/public/vendor/codemirror/chunks/properties-NN644MMA.js +1 -0
  270. package/neural-interface/public/vendor/codemirror/chunks/protobuf-YKWQK2J3.js +1 -0
  271. package/neural-interface/public/vendor/codemirror/chunks/pug-RIYXEKFW.js +1 -0
  272. package/neural-interface/public/vendor/codemirror/chunks/puppet-JZQ5WPEV.js +1 -0
  273. package/neural-interface/public/vendor/codemirror/chunks/python-EBCGGHAC.js +1 -0
  274. package/neural-interface/public/vendor/codemirror/chunks/q-5UMH2YQY.js +1 -0
  275. package/neural-interface/public/vendor/codemirror/chunks/r-7XMGG453.js +1 -0
  276. package/neural-interface/public/vendor/codemirror/chunks/rpm-IEV7TSLR.js +1 -0
  277. package/neural-interface/public/vendor/codemirror/chunks/ruby-T4SPFPNM.js +1 -0
  278. package/neural-interface/public/vendor/codemirror/chunks/sas-GEDX67ET.js +1 -0
  279. package/neural-interface/public/vendor/codemirror/chunks/scheme-FTLOWKPC.js +1 -0
  280. package/neural-interface/public/vendor/codemirror/chunks/shell-HUFRKFSJ.js +1 -0
  281. package/neural-interface/public/vendor/codemirror/chunks/sieve-P5QUDVYO.js +1 -0
  282. package/neural-interface/public/vendor/codemirror/chunks/smalltalk-2SORJNQI.js +1 -0
  283. package/neural-interface/public/vendor/codemirror/chunks/solr-M22TGAR5.js +1 -0
  284. package/neural-interface/public/vendor/codemirror/chunks/sparql-4M3KL752.js +1 -0
  285. package/neural-interface/public/vendor/codemirror/chunks/spreadsheet-3K23LA4N.js +1 -0
  286. package/neural-interface/public/vendor/codemirror/chunks/sql-VHRFMUBI.js +1 -0
  287. package/neural-interface/public/vendor/codemirror/chunks/stex-QZDAQUY2.js +1 -0
  288. package/neural-interface/public/vendor/codemirror/chunks/stylus-7ZDKAKBD.js +1 -0
  289. package/neural-interface/public/vendor/codemirror/chunks/swift-WCLCLGMR.js +1 -0
  290. package/neural-interface/public/vendor/codemirror/chunks/tcl-JLMU6WOK.js +1 -0
  291. package/neural-interface/public/vendor/codemirror/chunks/textile-4CHDKZ4T.js +1 -0
  292. package/neural-interface/public/vendor/codemirror/chunks/tiddlywiki-DKMCZZQ4.js +1 -0
  293. package/neural-interface/public/vendor/codemirror/chunks/tiki-F4JIG36Q.js +1 -0
  294. package/neural-interface/public/vendor/codemirror/chunks/toml-BHMHTIM2.js +1 -0
  295. package/neural-interface/public/vendor/codemirror/chunks/troff-YQ2KV5NT.js +1 -0
  296. package/neural-interface/public/vendor/codemirror/chunks/ttcn-ZHRCVFJY.js +1 -0
  297. package/neural-interface/public/vendor/codemirror/chunks/ttcn-cfg-KJOVTY4X.js +1 -0
  298. package/neural-interface/public/vendor/codemirror/chunks/turtle-535P5N6I.js +1 -0
  299. package/neural-interface/public/vendor/codemirror/chunks/vb-76UMQHOT.js +1 -0
  300. package/neural-interface/public/vendor/codemirror/chunks/vbscript-CGNDTK6Z.js +1 -0
  301. package/neural-interface/public/vendor/codemirror/chunks/velocity-VSJDO5T5.js +1 -0
  302. package/neural-interface/public/vendor/codemirror/chunks/verilog-4T6ETXIP.js +1 -0
  303. package/neural-interface/public/vendor/codemirror/chunks/vhdl-T7FNAXUN.js +1 -0
  304. package/neural-interface/public/vendor/codemirror/chunks/webidl-P6MJSJ6J.js +1 -0
  305. package/neural-interface/public/vendor/codemirror/chunks/xquery-UOVILRGN.js +1 -0
  306. package/neural-interface/public/vendor/codemirror/chunks/yacas-CV2APROL.js +1 -0
  307. package/neural-interface/public/vendor/codemirror/chunks/z80-3KFNVM4M.js +1 -0
  308. package/neural-interface/public/vendor/codemirror/codemirror-6.43.14-75f05e67.min.js +2 -0
  309. package/neural-interface/public/vendor/codemirror/manifest.json +60 -0
  310. package/neural-interface/scripts/build-vendor.mjs +78 -2
  311. package/neural-interface/scripts/vendor-codemirror-entry.js +121 -0
  312. package/neural-interface/server.js +873 -1028
  313. package/neural-interface/templates/rulesets/README.md +1 -1
  314. package/package.json +2 -8
  315. package/setup.js +44 -7
  316. package/benchmarks/browser-live-results.json +0 -79
  317. package/benchmarks/browser-v2-results.json +0 -258
  318. package/neural-interface/lib/codex-capabilities.d.ts +0 -41
  319. package/neural-interface/public/category-logos/claude.svg +0 -1
  320. package/neural-interface/public/category-logos/gemini-color.svg +0 -1
  321. package/neural-interface/public/category-logos/openai.svg +0 -1
  322. package/neural-interface/public/category-logos/synabun.png +0 -0
  323. package/neural-interface/public/claude.svg +0 -1
  324. package/neural-interface/public/gallery.svg +0 -6
  325. package/neural-interface/public/leonardoai.svg +0 -1
  326. package/neural-interface/public/mcp.svg +0 -4
  327. package/neural-interface/public/opencode-logo-dark-square.svg +0 -1
  328. package/neural-interface/public/pin-svgrepo-com.svg +0 -25
  329. package/neural-interface/public/sessionicon.svg +0 -8
  330. package/neural-interface/public/toolbar-album.svg +0 -8
  331. package/neural-interface/public/toolbar-question.svg +0 -6
  332. package/neural-interface/public/toolbar-sessions.svg +0 -6
  333. package/neural-interface/scripts/codex-protocol-fixture.mjs +0 -83
  334. package/skills/synabun/synabunicon.png +0 -0
@@ -28,21 +28,29 @@ import { getHiddenModels, HIDDEN_MODELS_EVENT, hiddenModelMatch } from '../ocp-h
28
28
  import { getProviderMeta } from '../provider-icons.js';
29
29
  import { on } from '../state.js';
30
30
  import { fetchAssistantCatalog, fetchProjects } from './asst-api.js';
31
- import { closeMenu, isMenuOpen, openMenu } from './asst-menu.js';
31
+ import { closeMenu, currentMenu, isMenuOpen, openMenu } from './asst-menu.js';
32
32
  import {
33
33
  ACCOUNT_PROVIDERS,
34
34
  brainRequiresSwitch,
35
+ emptyProjectView,
35
36
  modelShortName,
36
37
  normalizeBrain,
37
38
  PERMISSION_MODES,
38
39
  PLAN_MODE_PROVIDERS,
39
40
  permissionModeLabel,
40
41
  PROVIDERS,
42
+ projectLabel,
41
43
  resolveBrainDefaults,
44
+ TEST_DENSITIES,
45
+ normalizeTestDensity,
46
+ testDensityBars,
42
47
  } from './asst-state.js';
43
48
 
44
49
  let _projectsCache = null;
45
50
 
51
+ /** SynaBun's registered projects as the last load saw them ([{ path, label }]; [] before the first). */
52
+ export function cachedProjects() { return _projectsCache || []; }
53
+
46
54
  const ICON_FOLDER = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>';
47
55
  const ICON_SHIELD = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3 5 6v5c0 4.5 3 8 7 10 4-2 7-5.5 7-10V6z"/></svg>';
48
56
  const ICON_PLUG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3v5M15 3v5M6 8h12v3a6 6 0 0 1-12 0zM12 17v4"/></svg>';
@@ -53,6 +61,25 @@ export const ICON_ACCOUNT = '<svg viewBox="0 0 24 24" fill="none" stroke="curren
53
61
  /** Dropdown caret shared by every toolbar dropdown (fields, route mode). */
54
62
  export const ICON_CARET = '<svg viewBox="0 0 10 10" aria-hidden="true"><path d="M2.5 3.75 5 6.25l2.5-2.5"/></svg>';
55
63
 
64
+ // Test density: three bars centred in the 24 box (x 6–18, y 6–18; 5–19 with the stroke), `filled` of them at full strength.
65
+ function testsIcon(filled) {
66
+ const bar = (index, d) => `<path d="${d}"${index < filled ? '' : ' opacity=".3"'}/>`;
67
+ return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${bar(0, 'M6 18v-4')}${bar(1, 'M12 18v-8')}${bar(2, 'M18 18V6')}</svg>`;
68
+ }
69
+ // English copy of the Test density menu (i18n: assistant.tests.levels.* / assistant.tests.desc.*).
70
+ const TEST_DENSITY_COPY = {
71
+ none: ['None', 'No tests. Only checks that the change loads.'],
72
+ light: ['Light', 'Runs the tests that cover the change. New tests only when they matter.'],
73
+ standard: ['Standard', 'Adds focused tests and runs the area\'s suite once.'],
74
+ thorough: ['Thorough', 'Full coverage, whole suite, live checks.'],
75
+ };
76
+
77
+ /** "Light" — the level's name (a missing or unknown level is the default one). */
78
+ export function testDensityLabel(level, t) {
79
+ const id = normalizeTestDensity(level);
80
+ return tf(t)(`assistant.tests.levels.${id}`, TEST_DENSITY_COPY[id][0]);
81
+ }
82
+
56
83
  function el(tag, className, text) {
57
84
  const node = document.createElement(tag);
58
85
  if (className) node.className = className;
@@ -92,6 +119,9 @@ let _catalog = null;
92
119
  let _catalogAt = 0;
93
120
  let _catalogPartial = false; // built without the server's catalog (no context variants)
94
121
  let _catalogLoading = null;
122
+ let _catalogEpoch = 0; // bumped by every invalidation: a load that began before it is not kept
123
+ let _catalogForceNext = false; // the lists changed: the next load asks the server for a new build
124
+ const _catalogListeners = new Set();
95
125
  const CATALOG_TTL_MS = 60_000;
96
126
 
97
127
  function trimNumber(n) {
@@ -150,11 +180,16 @@ let _hidden = { 'claude-code': new Set(), codex: new Set(), opencode: new Set()
150
180
 
151
181
  function hiddenSets(remote) {
152
182
  const out = {};
183
+ // Entries with a pin record (an alias hidden as the model it named then): each hides its own row only.
184
+ const rowOnly = {};
153
185
  for (const provider of PROVIDERS) {
154
186
  const list = Array.isArray(remote?.hidden?.[provider]) ? remote.hidden[provider] : [];
155
187
  out[provider] = new Set(list.map(id => String(id).toLowerCase()));
188
+ const pins = remote?.hiddenPins?.[provider];
189
+ rowOnly[provider] = new Set(pins && typeof pins === 'object' ? Object.keys(pins).map(id => id.toLowerCase()) : []);
156
190
  }
157
191
  out.opencode = new Set([...(out.opencode || []), ...hiddenOpencodeIds(remote)]);
192
+ out.rowOnly = rowOnly;
158
193
  return out;
159
194
  }
160
195
 
@@ -163,6 +198,9 @@ function hiddenSets(remote) {
163
198
  * id, or the model it resolves to at the same context window ("opus[1m]" →
164
199
  * "claude-opus-5-5[1m]"); "opus" and "opus[1m]" hide independently. OpenCode:
165
200
  * full or bare id (this window's Settings → OpenCode mirror counts too).
201
+ * A hidden alias hides the model it named when it was hidden, and the server
202
+ * keeps the list true to that (an alias that moved to another model is taken
203
+ * off it); such an entry hides its own row only, never through `resolved`.
166
204
  */
167
205
  export function hiddenModelFor(provider, id, { resolved = null, sets = _hidden } = {}) {
168
206
  const want = String(id || '').trim().toLowerCase();
@@ -173,22 +211,87 @@ export function hiddenModelFor(provider, id, { resolved = null, sets = _hidden }
173
211
  const suffix = (want.match(/\[(1m|extended)\]$/) || [''])[0];
174
212
  const up = String(resolved || '').trim().toLowerCase().replace(/\[(1m|extended)\]$/, '');
175
213
  const key = up ? `${up}${suffix}` : '';
176
- return key && key !== want && list.has(key) ? key : null;
214
+ return key && key !== want && list.has(key) && !sets.rowOnly?.[provider]?.has(key) ? key : null;
177
215
  }
178
216
 
179
217
  /**
180
- * { 'claude-code': [...], codex: [...], opencode: [...] } — the runtime option
181
- * lists the sidepanels use, enriched with tier/vision/price from
182
- * GET /api/assistant/catalog (which alone is used when a local list is empty).
183
- * Models the user hid (Assistant → Models, and Settings → OpenCode for
184
- * OpenCode) never appear, from either list.
218
+ * One provider's menu rows. Which models exist is the server catalog's word: it
219
+ * is built from the list the provider's own selector reads, with the models the
220
+ * user hid already left out. The window's runtime list (`local`) only adds what
221
+ * the catalog does not carry (the tagline, the default / top tag, a legacy
222
+ * selector). It stands in for the catalog only when the catalog has no row for
223
+ * the provider (request failed, older server, provider not answering there), or
224
+ * holds the provider's static stand-in while this window has a verified list.
225
+ */
226
+ export function mergeProviderModels(provider, remoteList, localList, { sets = _hidden, remoteSource = null } = {}) {
227
+ const remote = (Array.isArray(remoteList) ? remoteList : []).filter(m => m?.id);
228
+ const local = (Array.isArray(localList) ? localList : []).filter(m => m?.id || m?.model);
229
+ const localVerified = local.length > 0 && !local.some(m => m.catalogSource === 'fallback');
230
+ const out = [];
231
+ const seen = new Set();
232
+ const push = (m) => { if (m && !seen.has(m.id)) { seen.add(m.id); out.push(m); } };
233
+ if (remote.length && !(remoteSource === 'fallback' && localVerified)) {
234
+ const own = new Map(local.map(m => [String(m.id || m.model), m]));
235
+ for (const r of remote) {
236
+ if (hiddenModelFor(provider, r.id, { resolved: r.upstream || null, sets })) continue;
237
+ const mine = own.get(String(r.id)) || null;
238
+ // The catalog row first. What it does not carry (an older server, a row with no
239
+ // label or effort list) is read from the window's row for the same id.
240
+ const row = normalizeCatalogModel(
241
+ { ...r, desc: mine?.desc || r.desc || r.description || '', tier: mine?.tier || r.tier || null, upstream: r.upstream || mine?.resolvedModel || null },
242
+ mine ? {
243
+ label: mine.label || mine.displayName || '', efforts: modelEffortIds(mine) || undefined, contextWindow: mine.contextWindow || null,
244
+ defaultEffort: mine.defaultEffort || mine.defaultReasoningEffort || null, isDefault: mine.isDefault,
245
+ } : {},
246
+ );
247
+ if (row) row.selector = mine ? (modelSelectorValue(mine) || row.id) : row.id;
248
+ push(row);
249
+ }
250
+ return out;
251
+ }
252
+ const byId = new Map(remote.map(m => [String(m.id), m]));
253
+ for (const m of local) {
254
+ const extra = byId.get(String(m?.id)) || {};
255
+ if (hiddenModelFor(provider, m.id || m.model, { resolved: extra.upstream || m.resolvedModel || null, sets })) continue;
256
+ push(normalizeCatalogModel(m, extra));
257
+ }
258
+ return out;
259
+ }
260
+
261
+ /**
262
+ * The catalog's empty list for `provider` is its answer, not a gap: the list was
263
+ * read (`sources`: live, or the last verified one) and the provider said it has
264
+ * no model (`known`: OpenCode with nothing connected) or listed some and the
265
+ * user hid every one (`listed`). Not an answer: no list or no `sources` (request
266
+ * failed, an older server), a provider that did not answer ('unavailable'), and
267
+ * the server's static stand-in while this window holds a verified list (the
268
+ * same precedence as mergeProviderModels).
269
+ */
270
+ function catalogSaysNone(remote, provider, local) {
271
+ const rows = remote?.models?.[provider];
272
+ if (!Array.isArray(rows) || rows.length) return false;
273
+ const source = remote.sources?.[provider];
274
+ if (source !== 'live' && source !== 'stale' && source !== 'fallback') return false;
275
+ if (!(Number(remote.listed?.[provider]) > 0) && remote.known?.[provider] !== true) return false;
276
+ return !(source === 'fallback' && local.length > 0 && !local.some(m => m.catalogSource === 'fallback'));
277
+ }
278
+
279
+ /**
280
+ * { 'claude-code': [...], codex: [...], opencode: [...] }: the models of every
281
+ * Assistant model menu and of the Routes editor, from GET /api/assistant/catalog
282
+ * (see mergeProviderModels). Models the user hid (Assistant → Models, and
283
+ * Settings → OpenCode for OpenCode) never appear. `force` asks the server for a
284
+ * new build instead of its cached one.
185
285
  */
186
286
  export async function loadModelCatalog({ force = false } = {}) {
187
287
  if (!force && _catalog && Date.now() - _catalogAt < CATALOG_TTL_MS) return _catalog;
188
288
  if (_catalogLoading) return _catalogLoading;
289
+ const epoch = _catalogEpoch;
290
+ const serverForce = force || _catalogForceNext;
291
+ _catalogForceNext = false;
189
292
  _catalogLoading = (async () => {
190
293
  const [remote] = await Promise.all([
191
- fetchAssistantCatalog().catch(() => null),
294
+ fetchAssistantCatalog({ force: serverForce }).catch(() => null),
192
295
  ...PROVIDERS.map(p => Promise.resolve(ensureModelsForProfile(p, force)).catch(() => [])),
193
296
  ]);
194
297
  const out = {};
@@ -196,36 +299,16 @@ export async function loadModelCatalog({ force = false } = {}) {
196
299
  const sets = remote ? hiddenSets(remote) : { ..._hidden, opencode: new Set([...(_hidden.opencode || []), ...hiddenOpencodeIds(null)]) };
197
300
  _hidden = sets;
198
301
  for (const provider of PROVIDERS) {
199
- const remoteList = Array.isArray(remote?.models?.[provider]) ? remote.models[provider] : [];
200
- const byId = new Map(remoteList.filter(m => m?.id).map(m => [String(m.id), m]));
201
302
  const local = getModelsForProfile(provider) || [];
202
- const source = local.length ? local : remoteList;
203
- const seen = new Set();
204
- out[provider] = source.map(m => normalizeCatalogModel(m, byId.get(String(m?.id)) || {})).filter((m) => {
205
- if (!m || seen.has(m.id)) return false;
206
- const extra = byId.get(String(m.id)) || {};
207
- if (hiddenModelFor(provider, m.id, { resolved: extra.upstream || m.resolvedModel || null, sets })) return false;
208
- seen.add(m.id);
209
- return true;
210
- });
211
- // Context variants ("opus[1m]", "gpt-x[extended]") the local list lacks: the
212
- // catalog lists every one, each right after its base model.
213
- if (provider !== 'opencode' && source !== remoteList) {
214
- for (const r of remoteList) {
215
- if (!/\[(1m|extended)\]$/i.test(String(r?.id || '')) || seen.has(String(r.id))) continue;
216
- if (hiddenModelFor(provider, r.id, { resolved: r.upstream || null, sets })) continue;
217
- const m = normalizeCatalogModel(r, r);
218
- if (!m) continue;
219
- seen.add(m.id);
220
- const base = String(r.id).replace(/\[(1m|extended)\]$/i, '');
221
- const at = out[provider].findIndex(x => x.id === base);
222
- if (at >= 0) out[provider].splice(at + 1, 0, m); else out[provider].push(m);
223
- }
224
- }
303
+ // An empty list the catalog vouches for is its answer: the window's own rows do not stand in for it.
304
+ out[provider] = catalogSaysNone(remote, provider, local)
305
+ ? []
306
+ : mergeProviderModels(provider, remote?.models?.[provider], local, { sets, remoteSource: remote?.sources?.[provider] || null });
225
307
  }
226
308
  _catalog = out;
227
- // Built without the server's catalog: not cached, the next open asks again.
228
- _catalogAt = remote ? Date.now() : 0;
309
+ // Not kept when built without the server's catalog, or when the lists changed
310
+ // while it loaded: the next open asks again.
311
+ _catalogAt = remote && epoch === _catalogEpoch ? Date.now() : 0;
229
312
  _catalogPartial = !remote;
230
313
  return out;
231
314
  })().finally(() => { _catalogLoading = null; });
@@ -235,6 +318,11 @@ export async function loadModelCatalog({ force = false } = {}) {
235
318
  /** The last loadModelCatalog() result, synchronously (null before the first load). */
236
319
  export function cachedModelCatalog() { return _catalog; }
237
320
 
321
+ /** UI metadata from the same catalog load; no second list or extra endpoint. */
322
+ export function modelCatalogStatus() {
323
+ return { partial: _catalogPartial, hiddenCounts: Object.fromEntries(PROVIDERS.map(provider => [provider, _hidden[provider]?.size || 0])) };
324
+ }
325
+
238
326
  /** The catalog row for provider/model (covers "[1m]" / "[extended]" ids), else null. */
239
327
  export function catalogModelRow(provider, model, catalog = _catalog) {
240
328
  if (!provider || !model) return null;
@@ -246,6 +334,27 @@ export function invalidateModelCatalog() {
246
334
  _catalog = null;
247
335
  _catalogAt = 0;
248
336
  _catalogPartial = false;
337
+ _catalogEpoch += 1;
338
+ }
339
+
340
+ /**
341
+ * Tells `listener` whenever the lists behind loadModelCatalog() changed: a model
342
+ * hidden or shown, a provider listing other models (the server's
343
+ * `assistant:catalog-changed`), an OpenCode provider connected or removed. The
344
+ * cache is already dropped when it is called. Returns the function that stops it.
345
+ */
346
+ export function onModelCatalogChange(listener) {
347
+ if (typeof listener !== 'function') return () => {};
348
+ _catalogListeners.add(listener);
349
+ return () => { _catalogListeners.delete(listener); };
350
+ }
351
+
352
+ function modelListsChanged() {
353
+ invalidateModelCatalog();
354
+ _catalogForceNext = true;
355
+ for (const listener of [..._catalogListeners]) {
356
+ try { listener(); } catch { /* a listener's failure is its own */ }
357
+ }
249
358
  }
250
359
 
251
360
  /**
@@ -267,19 +376,25 @@ export function resolvableModels(provider, keep = '', catalog = _catalog) {
267
376
  return hiddenModelFor(provider, keep) || unsure ? [...rows, { id: keep, selector: keep }] : rows;
268
377
  }
269
378
 
270
- // A Settings → OpenCode checkbox or an Assistant → Models toggle (this window, or another via sync).
271
- if (typeof document !== 'undefined') document.addEventListener(HIDDEN_MODELS_EVENT, invalidateModelCatalog);
272
- on('sync:opencode:hidden-models-changed', invalidateModelCatalog);
273
- on('sync:assistant:hidden-models-changed', invalidateModelCatalog);
379
+ // A Settings → OpenCode checkbox or an Assistant → Models toggle (this window, or another via
380
+ // sync), an OpenCode provider connected or removed in Settings, a provider listing other models.
381
+ if (typeof document !== 'undefined') {
382
+ document.addEventListener(HIDDEN_MODELS_EVENT, modelListsChanged);
383
+ document.addEventListener('ocp-providers-changed', modelListsChanged);
384
+ }
385
+ on('sync:opencode:hidden-models-changed', modelListsChanged);
386
+ on('sync:assistant:hidden-models-changed', modelListsChanged);
387
+ on('sync:assistant:catalog-changed', modelListsChanged);
274
388
 
275
389
  function sameModel(selected, m) {
276
390
  if (!selected) return false;
277
391
  return selected === m.id || selected === m.selector;
278
392
  }
279
393
 
280
- function modelDesc(m, tr) {
394
+ function modelDesc(m, tr, showUnknown = false) {
281
395
  return [
282
- m.ctxLabel, m.desc, fmtModelPrice(m.price), m.vision === true ? tr('assistant.brain.vision', 'vision') : '',
396
+ m.ctxLabel || (showUnknown ? m.contextWindow ? tr('assistant.routes.menu.contextTokens', '{count} tokens', { count: m.contextWindow.toLocaleString() }) : tr('assistant.routes.menu.contextUnknown', 'Context unknown') : ''), m.desc,
397
+ fmtModelPrice(m.price) || (showUnknown ? tr('assistant.routes.menu.priceUnknown', 'Price unknown') : ''), m.vision === true ? tr('assistant.brain.vision', 'vision') : '',
283
398
  m.outputs?.image === true ? tr('assistant.brain.imageOut', 'makes images') : '',
284
399
  m.outputs?.video === true ? tr('assistant.brain.videoOut', 'makes video') : '',
285
400
  ].filter(Boolean).join(' · ');
@@ -309,9 +424,19 @@ export async function openModelMenu(anchor, opts = {}) {
309
424
  const tr = tf(t);
310
425
  const b = normalizeBrain(brain);
311
426
  anchor.setAttribute('aria-busy', 'true');
427
+ // Open immediately: a slow provider still has a visible, cancellable state.
428
+ const loading = opts.routingEditor ? openMenu(anchor, {
429
+ title, className: 'asst-model-menu asst-route-model-menu',
430
+ items: [{ kind: 'info', label: tr('assistant.routes.editorState.loadingModels', 'Loading models…') }],
431
+ width: 380, zIndex: opts.zIndex ?? null, repositionOnResize: true,
432
+ }) : null;
433
+ if (loading) loading.el.setAttribute('aria-busy', 'true');
434
+ if (loading) { loading.el.setAttribute('role', 'dialog'); anchor.setAttribute('aria-haspopup', 'dialog'); }
312
435
  let catalog = {};
313
- try { catalog = await loadModelCatalog(); } catch { catalog = {}; } finally { anchor.removeAttribute('aria-busy'); }
314
- if (!anchor.isConnected) return null;
436
+ let failed = false;
437
+ try { catalog = await loadModelCatalog({ force: opts.forceReload === true }); } catch { failed = true; } finally { anchor.removeAttribute('aria-busy'); }
438
+ if (!anchor.isConnected || (loading && currentMenu() !== loading.el)) return null;
439
+ const metadata = modelCatalogStatus();
315
440
  const items = [];
316
441
  if (includeHere) {
317
442
  items.push({ kind: 'radio', id: '__here', label: hereLabel || tr('assistant.routes.brainDecides', 'Let the brain decide'), selected: !current, onSelect: () => onSelect?.({ here: true }) });
@@ -320,26 +445,30 @@ export async function openModelMenu(anchor, opts = {}) {
320
445
  let listed = 0;
321
446
  for (const provider of list) {
322
447
  const models = (catalog[provider] || []).filter(m => typeof accept !== 'function' || accept(provider, m));
323
- if (!models.length) continue;
448
+ if (!models.length && !opts.routingEditor) continue;
324
449
  listed += models.length;
325
450
  const meta = getProviderMeta(provider);
326
- const note = warnSwitch && provider !== b.provider ? tr('assistant.brain.newConversation', 'starts a new conversation') : '';
451
+ const hiddenCount = metadata.hiddenCounts[provider] || 0;
452
+ const note = opts.routingEditor ? tr('assistant.routes.menu.hiddenCount', '{count} hidden in Models', { count: hiddenCount }) : warnSwitch && provider !== b.provider ? tr('assistant.brain.newConversation', 'starts a new conversation') : '';
327
453
  items.push({ kind: 'header', label: meta.label, icon: meta.icon, color: meta.color, note });
328
454
  for (const m of models) {
329
455
  items.push({
330
456
  kind: 'radio',
331
457
  id: `${provider}:${m.id}`,
332
458
  label: m.label,
333
- desc: modelDesc(m, tr),
459
+ desc: modelDesc(m, tr, opts.routingEditor),
460
+ title: `${m.label || m.id} · ${m.id}`,
334
461
  tag: tierTag(m, tr),
335
462
  keywords: `${meta.label} ${m.id}`,
336
463
  selected: !!current && current.provider === provider && sameModel(current.model, m),
337
464
  onSelect: () => onSelect?.({ provider, model: m.id, label: m.label, tier: m.tier || null, vision: m.vision ?? null, outputs: m.outputs || null, efforts: m.efforts || null, price: m.price || null }),
338
465
  });
339
466
  }
467
+ if (opts.routingEditor && !models.length) items.push({ kind: 'info', label: typeof accept === 'function' ? tr('assistant.routes.menu.noCompatible', 'No compatible models shown for this provider.') : tr('assistant.routes.menu.providerEmpty', 'No models shown for this provider.') });
340
468
  }
341
469
  // Nothing passes the filter (e.g. no model makes video yet): say so instead of an empty list.
342
470
  if (!listed && typeof accept === 'function' && emptyLabel) items.push({ kind: 'info', label: emptyLabel });
471
+ if (opts.routingEditor && !listed && typeof accept !== 'function') items.push({ kind: 'info', label: tr('assistant.routes.editorState.modelsEmpty', 'No models are available. Check Models or connect a provider, then reload.') });
343
472
  if (allowCustom) {
344
473
  items.push({ kind: 'separator' });
345
474
  items.push({
@@ -356,29 +485,63 @@ export async function openModelMenu(anchor, opts = {}) {
356
485
  },
357
486
  });
358
487
  }
359
- if (typeof opts.onManageModels === 'function') {
488
+ if (!opts.routingEditor && typeof opts.onManageModels === 'function') {
360
489
  items.push({ id: '__manage_models', label: tr('assistant.models.manage', 'Manage models…'), onSelect: () => opts.onManageModels() });
361
490
  }
362
- return openMenu(anchor, {
491
+ const menu = openMenu(anchor, {
363
492
  title: title || tr('assistant.brain.model', 'Model'),
364
493
  items,
365
494
  filter: true,
366
495
  filterPlaceholder: tr('assistant.brain.filterModels', 'Filter models…'),
367
496
  placement,
368
- width: 300,
497
+ width: opts.routingEditor ? 380 : 300,
369
498
  role: 'menu',
370
- className: 'asst-model-menu',
499
+ className: `asst-model-menu${opts.routingEditor ? ' asst-route-model-menu' : ''}`,
371
500
  zIndex: opts.zIndex ?? null,
501
+ repositionOnResize: opts.routingEditor === true,
502
+ filterEmptyLabel: tr('assistant.routes.menu.noMatches', 'No matching models. Check Models for hidden choices.'),
372
503
  });
504
+ if (opts.routingEditor && menu) {
505
+ // Search and a custom-id input belong to a dialog; choices are a radio group.
506
+ // A menu role cannot contain these form fields or recovery buttons.
507
+ menu.el.setAttribute('role', 'dialog');
508
+ anchor.setAttribute('aria-haspopup', 'dialog');
509
+ const choices = menu.list.querySelectorAll('[role="menuitemradio"]');
510
+ for (const choice of choices) choice.setAttribute('role', 'radio');
511
+ const groupRole = () => {
512
+ const visible = [...choices].filter(n => !n.hidden);
513
+ const focusChoice = visible.find(n => n.getAttribute('aria-checked') === 'true') || visible[0];
514
+ for (const choice of choices) choice.tabIndex = choice === focusChoice ? 0 : -1;
515
+ menu.list.setAttribute('role', visible.length ? 'radiogroup' : 'group');
516
+ menu.list.setAttribute('aria-labelledby', `${menu.el.id}-title`);
517
+ };
518
+ groupRole(); menu.el.querySelector('.asst-dd-filter')?.addEventListener('input', groupRole);
519
+ // Outside the filter's scroller: recovery stays visible, including zero results.
520
+ const footer = document.createElement('div'); footer.className = 'asst-route-menu-foot';
521
+ const note = document.createElement('div'); note.className = 'asst-dd-info';
522
+ note.textContent = failed || metadata.partial ? tr('assistant.routes.menu.failed', 'Model list could not be refreshed. Provider lists may be out of date.') : tr('assistant.routes.menu.hiddenHelp', 'Missing a model? Your Models list controls what is shown.');
523
+ note.setAttribute('role', 'status'); footer.append(note);
524
+ const customInput = menu.list.querySelector('.asst-dd-input');
525
+ if (customInput) footer.append(customInput);
526
+ for (const [label, action] of [
527
+ ...(typeof opts.onManageModels === 'function' ? [[tr('assistant.models.manage', 'Manage models…'), () => { closeMenu(); opts.onManageModels(); }]] : []),
528
+ [tr('assistant.routes.reloadModels', 'Reload models'), () => { closeMenu(); openModelMenu(anchor, { ...opts, forceReload: true }); }],
529
+ ]) {
530
+ const button = document.createElement('button'); button.type = 'button'; button.className = 'asst-dd-item'; button.tabIndex = -1; button.textContent = label; button.addEventListener('click', action); footer.append(button);
531
+ }
532
+ menu.el.append(footer); menu.reposition();
533
+ }
534
+ return menu;
373
535
  }
374
536
 
375
537
  // ── Picker ──────────────────────────────────────────────────────────────────
376
538
 
377
- // Creation order is DOM order within a slot: model · effort · variant · mode · account in the composer.
539
+ // Creation order is DOM order within a slot: model · effort · variant · mode · tests · account in the composer.
378
540
  // `effort` is the OpenCode agent for OpenCode; `variant` is OpenCode's effort (the model's variants).
379
- const FIELDS = ['model', 'effort', 'variant', 'mode', 'account', 'project', 'mcp'];
541
+ // `tests` is the session's Test density, not a brain field: it shows only while the host supplies a level.
542
+ const FIELDS = ['model', 'effort', 'variant', 'mode', 'tests', 'account', 'project', 'mcp'];
380
543
  // Slot of each field: the composer row (the brain and how it acts) · the toolbar (where it works).
381
- const FIELD_GROUP = { model: 'brain', effort: 'brain', variant: 'brain', mode: 'brain', account: 'brain', project: 'context', mcp: 'context' };
544
+ const FIELD_GROUP = { model: 'brain', effort: 'brain', variant: 'brain', mode: 'brain', tests: 'brain', account: 'brain', project: 'context', mcp: 'context' };
382
545
 
383
546
  /**
384
547
  * createBrainPicker(host, hooks) — `host` is either one element (all fields in
@@ -386,7 +549,9 @@ const FIELD_GROUP = { model: 'brain', effort: 'brain', variant: 'brain', mode: '
386
549
  * hooks: { t, getBrain() → brain, onChange(nextBrain, { requiresSwitch, field }),
387
550
  * onAddAccount(provider), onManageAccounts(provider), onToast(text),
388
551
  * getMenuAnchor() → fallback anchor (the toolbar "⋯"),
389
- * menuPlacement: 'below' (default) | 'above' | 'auto' }
552
+ * menuPlacement: 'below' (default) | 'above' | 'auto',
553
+ * getTestDensity() → level | null (null hides the field), onTestDensity(level),
554
+ * onProjects(projects) — the registered projects were (re)loaded }
390
555
  */
391
556
  export function createBrainPicker(host, hooks = {}) {
392
557
  const t = tf(hooks.t);
@@ -415,6 +580,8 @@ export function createBrainPicker(host, hooks = {}) {
415
580
 
416
581
  let destroyed = false;
417
582
  let loading = null;
583
+ // The empty state's "Choose a project" (mountEmptyProject): the project field's second button.
584
+ let heroProject = null;
418
585
 
419
586
  function currentBrain() {
420
587
  return normalizeBrain(hooks.getBrain?.());
@@ -479,13 +646,19 @@ export function createBrainPicker(host, hooks = {}) {
479
646
  : (level ? level.label : (brain.effort || t('assistant.brain.effortOff', 'off'))),
480
647
  variantSupported: brain.provider === 'opencode' && hasEfforts,
481
648
  variant: level ? level.label : (brain.effort || t('assistant.brain.variantDefault', 'default variant')),
482
- project: brain.project ? basename(brain.project) : t('assistant.brain.projectHome', 'Home'),
649
+ project: brain.project ? projectLabel(brain.project, _projectsCache || []) : t('assistant.brain.projectHome', 'Home'),
483
650
  mode: permissionModeLabel(brain, (id, fallback) => t(`assistant.brain.modes.${id}`, fallback)),
484
651
  mcp: presets[brain.mcpProfile]?.label || brain.mcpProfile || 'full',
485
652
  account: ACCOUNT_PROVIDERS.has(brain.provider) ? accountLabel(brain) : '',
486
653
  };
487
654
  }
488
655
 
656
+ /** The session's Test density, or null while the host has none to show (a server that does not know it). */
657
+ function testDensity() {
658
+ const level = typeof hooks.getTestDensity === 'function' ? hooks.getTestDensity() : null;
659
+ return level ? normalizeTestDensity(level) : null;
660
+ }
661
+
489
662
  function setBtn(field, { label, icon, color, hidden, disabled, tooltip, aria }) {
490
663
  const btn = buttons[field];
491
664
  if (!btn) return;
@@ -520,7 +693,58 @@ export function createBrainPicker(host, hooks = {}) {
520
693
  setBtn('account', { label: v.account, icon: ICON_ACCOUNT, hidden: accountChoices < 2, tooltip: `${t('assistant.brain.account', 'Account profile')}: ${v.account}` });
521
694
  setBtn('project', { label: v.project, icon: ICON_FOLDER, tooltip: brain.project || t('assistant.brain.projectNoneTip', 'Working project: home directory'), aria: `${t('assistant.brain.project', 'Working project')}: ${brain.project || v.project}` });
522
695
  setBtn('mode', { label: v.mode, icon: ICON_SHIELD, tooltip: `${t('assistant.brain.mode', 'Permission mode')}: ${v.mode}` });
696
+ const tests = testDensity();
697
+ const testsLabel = tests ? testDensityLabel(tests, hooks.t) : '';
698
+ setBtn('tests', { label: testsLabel, icon: testsIcon(testDensityBars(tests)), hidden: !tests, tooltip: `${t('assistant.tests.title', 'Test density')}: ${testsLabel}` });
699
+ if (tests) buttons.tests.dataset.level = tests; else delete buttons.tests.dataset.level;
523
700
  setBtn('mcp', { label: v.mcp, icon: ICON_PLUG, tooltip: `${t('assistant.brain.mcp', 'MCP tool profile')}: ${v.mcp}` });
701
+ renderEmptyProject(brain);
702
+ }
703
+
704
+ function renderEmptyProject(brain = currentBrain()) {
705
+ const btn = heroProject;
706
+ if (!btn) return;
707
+ const view = emptyProjectView(brain.project, _projectsCache || [], t);
708
+ btn.dataset.chosen = view.chosen ? '1' : '0';
709
+ const key = btn.querySelector('.asst-empty-project-key');
710
+ key.textContent = view.key;
711
+ key.hidden = !view.key;
712
+ btn.querySelector('.asst-dd-label').textContent = view.label;
713
+ if (view.title) btn.setAttribute('data-tooltip', view.title); else btn.removeAttribute('data-tooltip');
714
+ btn.setAttribute('aria-label', view.aria);
715
+ }
716
+
717
+ /**
718
+ * The empty state's project picker, in `slot` (asst-render.js): one more
719
+ * button for the project field, drawn by render() like the toolbar's and
720
+ * opening the same menu, so the two always show the same project. A pick
721
+ * sends focus back to it. A new slot replaces the last one; null removes it.
722
+ */
723
+ function mountEmptyProject(slot) {
724
+ heroProject?.remove();
725
+ heroProject = null;
726
+ if (destroyed || !slot || typeof slot.appendChild !== 'function') return null;
727
+ const btn = el('button', 'asst-dd asst-empty-project-btn');
728
+ btn.type = 'button';
729
+ btn.dataset.field = 'project';
730
+ btn.setAttribute('aria-haspopup', 'menu');
731
+ btn.setAttribute('aria-expanded', 'false');
732
+ // glyph | seam | "Project" + name | caret: the field's capsule at the suggestions' md size.
733
+ const icon = el('span', 'asst-icon');
734
+ icon.innerHTML = ICON_FOLDER;
735
+ const caret = el('span', 'asst-dd-caret');
736
+ caret.innerHTML = ICON_CARET;
737
+ for (const part of [icon, caret]) part.setAttribute('aria-hidden', 'true');
738
+ btn.append(icon, el('span', 'asst-empty-project-key'), el('span', 'asst-dd-label'), caret);
739
+ btn.addEventListener('click', (e) => {
740
+ e.stopPropagation();
741
+ if (isMenuOpen(btn)) { closeMenu(); return; }
742
+ openField('project', btn, { restoreFocus: true });
743
+ });
744
+ slot.appendChild(btn);
745
+ heroProject = btn;
746
+ renderEmptyProject();
747
+ return btn;
524
748
  }
525
749
 
526
750
  /** Current values for the toolbar "⋯" menu (fields hidden inline stay reachable there). */
@@ -535,6 +759,7 @@ export function createBrainPicker(host, hooks = {}) {
535
759
  { field: 'variant', label: t('assistant.brain.variant', 'Model variant'), value: v.variant, icon: ICON_EFFORT, hidden: !v.variantSupported },
536
760
  { field: 'project', label: t('assistant.brain.project', 'Working project'), value: brain.project || v.project, icon: ICON_FOLDER, hidden: false },
537
761
  { field: 'mode', label: t('assistant.brain.mode', 'Permission mode'), value: v.mode, icon: ICON_SHIELD, hidden: false },
762
+ { field: 'tests', label: t('assistant.tests.title', 'Test density'), value: testDensity() ? testDensityLabel(testDensity(), hooks.t) : '', icon: testsIcon(testDensityBars(testDensity())), hidden: !testDensity() },
538
763
  { field: 'mcp', label: t('assistant.brain.mcp', 'MCP tool profile'), value: v.mcp, icon: ICON_PLUG, hidden: false },
539
764
  { field: 'account', label: t('assistant.brain.account', 'Account profile'), value: v.account, icon: ICON_ACCOUNT, hidden: !ACCOUNT_PROVIDERS.has(brain.provider) },
540
765
  ];
@@ -555,7 +780,12 @@ export function createBrainPicker(host, hooks = {}) {
555
780
  if (provider === 'codex') jobs.push(fetchCodexAccounts(force));
556
781
  if (provider === 'claude-code') jobs.push(fetchClaudeAccounts(force));
557
782
  if (provider === 'opencode') jobs.push(fetchOpencodeAgents(force));
558
- if (!_projectsCache || force) jobs.push(fetchProjects().then((p) => { _projectsCache = p; }).catch(() => { _projectsCache = _projectsCache || []; }));
783
+ if (!_projectsCache || force) {
784
+ jobs.push(fetchProjects().then((p) => {
785
+ _projectsCache = p;
786
+ if (!destroyed) { try { hooks.onProjects?.(p); } catch { /* the host's own failure */ } }
787
+ }).catch(() => { _projectsCache = _projectsCache || []; }));
788
+ }
559
789
  await Promise.allSettled(jobs);
560
790
  }
561
791
 
@@ -580,8 +810,11 @@ export function createBrainPicker(host, hooks = {}) {
580
810
  return fallback || btn || root;
581
811
  }
582
812
 
583
- /** Open a field menu; `anchorOverride` (e.g. the toolbar "⋯") when the inline button is hidden. */
584
- async function openField(field, anchorOverride = null) {
813
+ /**
814
+ * Open a field menu; `anchorOverride` (e.g. the toolbar "⋯") when the inline
815
+ * button is hidden. `restoreFocus`: a pick sends focus back to the anchor.
816
+ */
817
+ async function openField(field, anchorOverride = null, { restoreFocus = false } = {}) {
585
818
  const anchor = anchorFor(field, anchorOverride);
586
819
  const brain = currentBrain();
587
820
  if (field === 'model') {
@@ -601,6 +834,22 @@ export function createBrainPicker(host, hooks = {}) {
601
834
  });
602
835
  return;
603
836
  }
837
+ // The session's own setting: nothing to load.
838
+ if (field === 'tests') {
839
+ const current = testDensity();
840
+ if (!current) return;
841
+ openMenu(anchor, {
842
+ title: t('assistant.tests.title', 'Test density'),
843
+ items: TEST_DENSITIES.map((id) => ({
844
+ kind: 'radio', id, label: testDensityLabel(id, hooks.t),
845
+ desc: t(`assistant.tests.desc.${id}`, TEST_DENSITY_COPY[id][1]),
846
+ selected: id === current,
847
+ onSelect: () => { hooks.onTestDensity?.(id); render(); },
848
+ })),
849
+ placement, role: 'menu', className: 'asst-tests-menu',
850
+ });
851
+ return;
852
+ }
604
853
  // Everything else needs the provider catalog — load it lazily on first open.
605
854
  anchor.setAttribute('aria-busy', 'true');
606
855
  try { await ensureLoaded(brain.provider); } finally { anchor.removeAttribute('aria-busy'); }
@@ -657,10 +906,10 @@ export function createBrainPicker(host, hooks = {}) {
657
906
 
658
907
  if (field === 'project') {
659
908
  const projects = _projectsCache || [];
660
- const items = [{ kind: 'radio', id: '', label: t('assistant.brain.projectNone', 'Home directory'), desc: t('assistant.brain.projectNoneDesc', 'no project'), selected: !brain.project, onSelect: () => commit({ project: null }, 'project') }];
661
- for (const p of projects) items.push({ kind: 'radio', id: p.path, label: p.label || basename(p.path), desc: p.path, selected: p.path === brain.project, onSelect: () => commit({ project: p.path }, 'project') });
909
+ const items = [{ kind: 'radio', id: '', label: t('assistant.brain.projectNone', 'Home directory'), desc: t('assistant.brain.projectNoneDesc', 'no project'), selected: !brain.project, restoreFocus, onSelect: () => commit({ project: null }, 'project') }];
910
+ for (const p of projects) items.push({ kind: 'radio', id: p.path, label: p.label || basename(p.path), desc: p.path, selected: p.path === brain.project, restoreFocus, onSelect: () => commit({ project: p.path }, 'project') });
662
911
  items.push({ kind: 'separator' });
663
- items.push({ kind: 'input', placeholder: t('assistant.brain.projectCustom', 'Type a directory path…'), value: brain.project && !projects.some(p => p.path === brain.project) ? brain.project : '', onSubmit: (v) => commit({ project: v }, 'project') });
912
+ items.push({ kind: 'input', placeholder: t('assistant.brain.projectCustom', 'Type a directory path…'), value: brain.project && !projects.some(p => p.path === brain.project) ? brain.project : '', restoreFocus, onSubmit: (v) => commit({ project: v }, 'project') });
664
913
  menu(t('assistant.brain.project', 'Working project'), items, { width: 280, filter: projects.length > 8, filterPlaceholder: t('assistant.brain.filterProjects', 'Filter projects…') });
665
914
  return;
666
915
  }
@@ -737,8 +986,9 @@ export function createBrainPicker(host, hooks = {}) {
737
986
  openField,
738
987
  fieldSummary,
739
988
  modelLabel: () => modelLabel(),
989
+ mountEmptyProject,
740
990
  setBrain() { render(); },
741
991
  close: closeMenu,
742
- destroy() { destroyed = true; closeMenu(); Object.values(buttons).forEach((b) => b.remove()); root.remove(); },
992
+ destroy() { destroyed = true; closeMenu(); Object.values(buttons).forEach((b) => b.remove()); heroProject?.remove(); heroProject = null; root.remove(); },
743
993
  };
744
994
  }