synabun 2.0.9 → 2.0.10

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 (252) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +1 -1
  3. package/THIRD-PARTY-LICENSES.md +46 -4
  4. package/launcher.mjs +9 -3
  5. package/lib/start-launcher.js +6 -1
  6. package/mcp-server/package-lock.json +2 -2
  7. package/mcp-server/package.json +1 -1
  8. package/neural-interface/i18n/en.json +218 -7
  9. package/neural-interface/i18n/pt-BR.json +220 -9
  10. package/neural-interface/lib/account-usage.js +5 -3
  11. package/neural-interface/lib/app-controls-api.js +161 -0
  12. package/neural-interface/lib/assistant-api.js +21 -7
  13. package/neural-interface/lib/assistant-brains/opencode.js +13 -1
  14. package/neural-interface/lib/assistant-brains/opencode2-browser-policy/index.js +44 -0
  15. package/neural-interface/lib/assistant-brains/opencode2-route-gate/index.js +57 -0
  16. package/neural-interface/lib/assistant-catalog.js +17 -2
  17. package/neural-interface/lib/assistant-dispatch.js +3 -0
  18. package/neural-interface/lib/assistant-envelope.js +19 -1
  19. package/neural-interface/lib/assistant-runtime.js +127 -8
  20. package/neural-interface/lib/claude-model-catalog.js +64 -14
  21. package/neural-interface/lib/codex-settings-schema.js +109 -47
  22. package/neural-interface/lib/editor-completion-opencode.js +132 -35
  23. package/neural-interface/lib/exec-loop-command.js +8 -1
  24. package/neural-interface/lib/launch-cache.js +293 -0
  25. package/neural-interface/lib/login-start.js +524 -0
  26. package/neural-interface/lib/native-loop-providers.js +18 -3
  27. package/neural-interface/lib/native-loop-runtime.js +19 -0
  28. package/neural-interface/lib/opencode-db-schema.js +75 -0
  29. package/neural-interface/lib/opencode-history.js +16 -1
  30. package/neural-interface/lib/opencode-protocol.js +173 -0
  31. package/neural-interface/lib/opencode-provider-view.js +81 -0
  32. package/neural-interface/lib/opencode-runtime-config.js +10 -3
  33. package/neural-interface/lib/opencode-serve-records.js +606 -0
  34. package/neural-interface/lib/opencode-v2-client.js +33 -6
  35. package/neural-interface/lib/opencode-v2-ws-requests.js +19 -5
  36. package/neural-interface/lib/opencode-worker-activity.js +229 -0
  37. package/neural-interface/lib/opencode2-client.js +760 -0
  38. package/neural-interface/lib/opencode2-proxy.js +116 -0
  39. package/neural-interface/lib/opencode2-translate.js +1008 -0
  40. package/neural-interface/lib/process-identity.js +361 -0
  41. package/neural-interface/lib/session-cache-builder.js +26 -5
  42. package/neural-interface/lib/session-title-generator.js +18 -0
  43. package/neural-interface/lib/style-guide/api.js +4 -3
  44. package/neural-interface/lib/untrusted-file-headers.js +27 -0
  45. package/neural-interface/package-lock.json +1343 -45
  46. package/neural-interface/package.json +5 -1
  47. package/neural-interface/public/index.html +2 -2
  48. package/neural-interface/public/shared/agent-runtime-options.js +55 -11
  49. package/neural-interface/public/shared/assistant/asst-agents-dock.js +172 -17
  50. package/neural-interface/public/shared/assistant/asst-api.js +11 -3
  51. package/neural-interface/public/shared/assistant/asst-brain-picker.js +195 -80
  52. package/neural-interface/public/shared/assistant/asst-composer.js +1 -1
  53. package/neural-interface/public/shared/assistant/asst-hosts.js +49 -2
  54. package/neural-interface/public/shared/assistant/asst-menu.js +77 -6
  55. package/neural-interface/public/shared/assistant/asst-panel.js +197 -33
  56. package/neural-interface/public/shared/assistant/asst-rack-styles.js +28 -10
  57. package/neural-interface/public/shared/assistant/asst-redact.js +186 -0
  58. package/neural-interface/public/shared/assistant/asst-render.js +382 -36
  59. package/neural-interface/public/shared/assistant/asst-run-stage.js +231 -0
  60. package/neural-interface/public/shared/assistant/asst-sanitize.js +193 -3
  61. package/neural-interface/public/shared/assistant/asst-scene-styles.js +273 -0
  62. package/neural-interface/public/shared/assistant/asst-scenes.js +528 -0
  63. package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +3 -15
  64. package/neural-interface/public/shared/assistant/asst-state.js +14 -0
  65. package/neural-interface/public/shared/assistant/asst-styles.js +43 -0
  66. package/neural-interface/public/shared/assistant/asst-tool-kinds.js +13 -1
  67. package/neural-interface/public/shared/cc-dropdown.js +480 -0
  68. package/neural-interface/public/shared/cdx/cdx-panel.js +13 -1
  69. package/neural-interface/public/shared/cdx/cdx-render.js +15 -3
  70. package/neural-interface/public/shared/cdx/cdx-styles.js +4 -0
  71. package/neural-interface/public/shared/cdx/cdx-tabs.js +149 -35
  72. package/neural-interface/public/shared/cp/cp-markdown.js +4 -57
  73. package/neural-interface/public/shared/data-cache.js +223 -0
  74. package/neural-interface/public/shared/desktop-bridge.js +11 -0
  75. package/neural-interface/public/shared/desktop-titlebar-view.js +78 -1
  76. package/neural-interface/public/shared/desktop-titlebar.js +214 -28
  77. package/neural-interface/public/shared/desktop-update-view.js +15 -5
  78. package/neural-interface/public/shared/file-links.js +73 -8
  79. package/neural-interface/public/shared/html-shell.js +74 -80
  80. package/neural-interface/public/shared/launch-prefetch.js +59 -0
  81. package/neural-interface/public/shared/ocp-v2/ocp-v2-caps.js +3 -2
  82. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +7 -2
  83. package/neural-interface/public/shared/ocp-v2/ocp-v2-modelpicker.js +54 -17
  84. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +93 -2
  85. package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +3 -2
  86. package/neural-interface/public/shared/ocp-v2/ocp-v2-providers.js +11 -9
  87. package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +15 -4
  88. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +1 -1
  89. package/neural-interface/public/shared/ocp-v2/ocp-v2-send-logic.js +401 -0
  90. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +16 -9
  91. package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +60 -0
  92. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +48 -19
  93. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +195 -19
  94. package/neural-interface/public/shared/opencode-providers.js +39 -0
  95. package/neural-interface/public/shared/schedules-view.js +318 -0
  96. package/neural-interface/public/shared/settings/settings-app-pane.js +335 -0
  97. package/neural-interface/public/shared/settings/settings-app.js +243 -0
  98. package/neural-interface/public/shared/settings/settings-data.js +88 -0
  99. package/neural-interface/public/shared/settings/settings-ia.js +17 -0
  100. package/neural-interface/public/shared/sidepanel-session-status.js +49 -0
  101. package/neural-interface/public/shared/styles.css +540 -371
  102. package/neural-interface/public/shared/ui-assistant.js +27 -5
  103. package/neural-interface/public/shared/ui-automation-studio.js +67 -27
  104. package/neural-interface/public/shared/ui-bookmarks.js +2 -1
  105. package/neural-interface/public/shared/ui-claude-panel.js +76 -8
  106. package/neural-interface/public/shared/ui-codex-panel.js +1 -0
  107. package/neural-interface/public/shared/ui-detail.js +6 -6
  108. package/neural-interface/public/shared/ui-file-links.js +1 -0
  109. package/neural-interface/public/shared/ui-image-gallery.js +6 -5
  110. package/neural-interface/public/shared/ui-invite.js +18 -2
  111. package/neural-interface/public/shared/ui-link.js +9 -9
  112. package/neural-interface/public/shared/ui-menubar.js +232 -33
  113. package/neural-interface/public/shared/ui-native-loop-router.js +2 -0
  114. package/neural-interface/public/shared/ui-opencode-panel-v2.js +1 -0
  115. package/neural-interface/public/shared/ui-schedules-studio.js +2099 -869
  116. package/neural-interface/public/shared/ui-sessions.js +15 -4
  117. package/neural-interface/public/shared/ui-settings.js +87 -36
  118. package/neural-interface/public/shared/ui-sidebar.js +4 -4
  119. package/neural-interface/public/shared/ui-sidepanel-session-entry.js +13 -0
  120. package/neural-interface/public/shared/ui-sidepanel-sessions.js +55 -4
  121. package/neural-interface/public/shared/ui-sidepanel-tray.js +84 -8
  122. package/neural-interface/public/shared/ui-skills.js +70 -12
  123. package/neural-interface/public/shared/ui-sync.js +7 -0
  124. package/neural-interface/public/shared/ui-terminal.js +36 -20
  125. package/neural-interface/public/shared/ui-trash.js +8 -1
  126. package/neural-interface/public/shared/ui-tutorial-steps.js +7 -3
  127. package/neural-interface/public/shared/ui-tutorial.js +15 -5
  128. package/neural-interface/public/shared/ui-update.js +83 -14
  129. package/neural-interface/public/shared/ui-whiteboard.js +2 -1
  130. package/neural-interface/public/variant/2d/main.js +4 -1
  131. package/neural-interface/public/variant/3d/main.js +3 -0
  132. package/neural-interface/public/vendor/highlight/LICENSE +29 -0
  133. package/neural-interface/public/vendor/highlight/github-dark-dimmed-11.12.0.min.css +9 -0
  134. package/neural-interface/public/vendor/highlight/highlight-11.12.0.min.js +1262 -0
  135. package/neural-interface/public/vendor/highlight/manifest.json +5 -0
  136. package/neural-interface/public/vendor/html2canvas/LICENSE +22 -0
  137. package/neural-interface/public/vendor/html2canvas/html2canvas-1.4.1.min.js +20 -0
  138. package/neural-interface/public/vendor/html2canvas/manifest.json +4 -0
  139. package/neural-interface/public/vendor/marked/LICENSE.md +44 -0
  140. package/neural-interface/public/vendor/marked/manifest.json +4 -0
  141. package/neural-interface/public/vendor/marked/marked-14.1.4.esm.js +2502 -0
  142. package/neural-interface/public/vendor/mermaid/LICENSE +21 -0
  143. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/abnfDiagram-PAR5IXJI.mjs +1 -0
  144. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/architecture-7GRP2DOG-M7G3SWCO.mjs +1 -0
  145. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/architectureDiagram-2PNWDENW.mjs +36 -0
  146. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/blockDiagram-PUWGUBOA.mjs +129 -0
  147. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/c4Diagram-3NLNHZP5.mjs +38 -0
  148. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-2NMSUPJH.mjs +1 -0
  149. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-32MCUW2P.mjs +206 -0
  150. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-3NPNU5GP.mjs +1 -0
  151. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-3SMGFRGC.mjs +1 -0
  152. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-4CAJECRU.mjs +14 -0
  153. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-4OADUZ5Q.mjs +10 -0
  154. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5IMINLNL.mjs +1 -0
  155. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5IQCK5RY.mjs +3 -0
  156. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5MVFYWFU.mjs +1 -0
  157. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-AQ6EADP3.mjs +1 -0
  158. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-AZZRMDJM.mjs +15 -0
  159. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-CLS4B6BI.mjs +168 -0
  160. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-DZP67EKU.mjs +70 -0
  161. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-EICP7K5X.mjs +1 -0
  162. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-GDY2UAB4.mjs +1 -0
  163. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-H7VHZCWX.mjs +1 -0
  164. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-IJBDOHL6.mjs +1 -0
  165. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JFFVMJIA.mjs +1 -0
  166. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JQRUD6KW.mjs +1 -0
  167. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JZGDI5JK.mjs +1 -0
  168. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-KD6SPIPN.mjs +2 -0
  169. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-NF366YI6.mjs +231 -0
  170. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-NUDPKBEF.mjs +32 -0
  171. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-OJINMHTS.mjs +1 -0
  172. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-Q6NGPIAH.mjs +2 -0
  173. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-QJSWEUOL.mjs +132 -0
  174. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-R7JDMUKV.mjs +1 -0
  175. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-RCXAM2GK.mjs +1 -0
  176. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-RT4MAWR3.mjs +1 -0
  177. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-SFMINX4W.mjs +1 -0
  178. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-SLDQ463B.mjs +88 -0
  179. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-STOV2HOB.mjs +1 -0
  180. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-T5OCTHI4.mjs +1 -0
  181. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-TCEHX7R3.mjs +1 -0
  182. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VQC2EELJ.mjs +1 -0
  183. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VRZKL3EH.mjs +1 -0
  184. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VTHDQ7BV.mjs +1 -0
  185. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VY2OSTJR.mjs +2 -0
  186. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-WUBWJARI.mjs +1 -0
  187. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-YBMQ5JGJ.mjs +2 -0
  188. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-YDMLL4PJ.mjs +1 -0
  189. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-Z6V44ARX.mjs +62 -0
  190. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZJJ7VQJP.mjs +339 -0
  191. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZNLK7HRR.mjs +1 -0
  192. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZQN7UHIL.mjs +1 -0
  193. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZS6VBONO.mjs +174 -0
  194. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZUNWM646.mjs +1 -0
  195. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/classDiagram-FGAMPOII.mjs +1 -0
  196. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/classDiagram-v2-AI5RJEK4.mjs +1 -0
  197. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cose-bilkent-Q72C2OJ3.mjs +1 -0
  198. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cynefin-OW5HDTMX-6OCTWOGG.mjs +1 -0
  199. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cynefinDiagram-2OOWUY4M.mjs +62 -0
  200. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/dagre-MPVFI544.mjs +4 -0
  201. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-AE4NWXFW.mjs +24 -0
  202. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-BUJID5UG.mjs +41 -0
  203. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-C44CZ5NI.mjs +24 -0
  204. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-EYOLEXXS.mjs +30 -0
  205. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-P7B6PWOJ.mjs +3 -0
  206. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ebnfDiagram-FTPZ3PV6.mjs +1 -0
  207. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/erDiagram-H3ERR26V.mjs +99 -0
  208. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/eventmodeling-NTZA5JFV-WKSWQHFS.mjs +1 -0
  209. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/flowDiagram-YHGXBVSY.mjs +1 -0
  210. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ganttDiagram-RWFTB7ED.mjs +292 -0
  211. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/gitGraph-4MIJSDKK-JH2HKYUO.mjs +1 -0
  212. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/gitGraphDiagram-MAR6GKOP.mjs +106 -0
  213. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/info-A6RAGUB7-3TX3OFWW.mjs +1 -0
  214. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/infoDiagram-LV547AFH.mjs +2 -0
  215. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ishikawaDiagram-PUJQGNDS.mjs +70 -0
  216. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/journeyDiagram-EW5QJT4W.mjs +139 -0
  217. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/kanban-definition-JSQERAZB.mjs +89 -0
  218. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/katex-CZ4GXH2S.mjs +257 -0
  219. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/mindmap-definition-VBJCLQLM.mjs +96 -0
  220. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/packet-AYTQ26CC-MKII6P3Y.mjs +1 -0
  221. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pegDiagram-7WILOQTJ.mjs +1 -0
  222. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pie-WAS4IAKB-6B4OPMJ5.mjs +1 -0
  223. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pieDiagram-EFA7CBSB.mjs +39 -0
  224. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/quadrantDiagram-WARVYA7P.mjs +7 -0
  225. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/radar-RG4KPBEZ-UPTR3XUL.mjs +1 -0
  226. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-74A4TZTK-MEFUPFMS.mjs +1 -0
  227. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-abnf-HS5TGJTU-R5YG2S5D.mjs +1 -0
  228. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-ebnf-LZEXJU2U-G5QEX2L6.mjs +1 -0
  229. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-peg-WCYAUIDC-H5I2ZRY2.mjs +1 -0
  230. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroadDiagram-5NIQVNLQ.mjs +1 -0
  231. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/requirementDiagram-SXREGOKZ.mjs +84 -0
  232. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sankeyDiagram-VV74M2VP.mjs +40 -0
  233. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sequenceDiagram-52ZFIFDL.mjs +162 -0
  234. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sizeCapture-FGA5AGYP.mjs +1 -0
  235. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/stateDiagram-3QAKPIVI.mjs +1 -0
  236. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/stateDiagram-v2-HN2P4RDZ.mjs +1 -0
  237. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/swimlanes-QLVP42QD.mjs +1 -0
  238. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/swimlanesDiagram-JKPDBFCN.mjs +8 -0
  239. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/timeline-definition-GS45XAXW.mjs +120 -0
  240. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/treeView-Q6P3EWNA-ZRJGANNQ.mjs +1 -0
  241. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/treemap-WGGIJYW6-K64VN5AS.mjs +1 -0
  242. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/vennDiagram-QIQU2ZRJ.mjs +34 -0
  243. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/wardley-WFR3VGLG-D67OWCCH.mjs +1 -0
  244. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/wardleyDiagram-U6YNM66N.mjs +78 -0
  245. package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/xychartDiagram-XJAZEDNM.mjs +7 -0
  246. package/neural-interface/public/vendor/mermaid/manifest.json +4 -0
  247. package/neural-interface/public/vendor/mermaid/mermaid-11.17.2.esm.min.mjs +14 -0
  248. package/neural-interface/scripts/build-vendor.mjs +84 -3
  249. package/neural-interface/server.js +1099 -286
  250. package/npm-shrinkwrap.json +2 -2
  251. package/package.json +1 -1
  252. package/skills/synabun/modules/schedule.md +1 -1
@@ -29,6 +29,7 @@ import {
29
29
  import { getHiddenModels, HIDDEN_MODELS_EVENT, hiddenModelMatch } from '../ocp-hidden-models.js';
30
30
  import { getProviderMeta } from '../provider-icons.js';
31
31
  import { on } from '../state.js';
32
+ import { registerLaunchPrefetch } from '../launch-prefetch.js';
32
33
  import { fetchAssistantCatalog, fetchProjects, patchClaudeAccount, patchCodexAccount, setClaudeWorkerAccount, setCodexWorkerAccount } from './asst-api.js';
33
34
  import { accountBadge, accountIdentity, accountName, accountText } from '../account-view.js';
34
35
  import { autoAccountLabel } from '../account-usage.js';
@@ -54,6 +55,9 @@ import {
54
55
  } from './asst-state.js';
55
56
 
56
57
  let _projectsCache = null;
58
+ // Providers whose field menus have everything they read loaded once (models, catalog, accounts or agents,
59
+ // presets, projects): their menus open from it at once and read it again in the background.
60
+ const _warmProviders = new Set();
57
61
 
58
62
  /** SynaBun's registered projects as the last load saw them ([{ path, label }]; [] before the first). */
59
63
  export function cachedProjects() { return _projectsCache || []; }
@@ -321,11 +325,41 @@ export async function loadModelCatalog({ force = false } = {}) {
321
325
  // while it loaded: the next open asks again.
322
326
  _catalogAt = remote && epoch === _catalogEpoch ? Date.now() : 0;
323
327
  _catalogPartial = !remote;
328
+ if (_catalogAt) keepCatalog(out, sets);
324
329
  return out;
325
330
  })().finally(() => { _catalogLoading = null; });
326
331
  return _catalogLoading;
327
332
  }
328
333
 
334
+ // The last complete catalog is also kept in sessionStorage (this tab only): after a reload the model menus
335
+ // paint from it at once. Read back, it is due: the first load asks the server again.
336
+ const CATALOG_STORE_KEY = 'synabun:assistant-model-catalog:v1';
337
+ function keepCatalog(catalog, sets) {
338
+ try {
339
+ if (typeof sessionStorage === 'undefined') return;
340
+ const hidden = {};
341
+ for (const provider of PROVIDERS) hidden[provider] = [...(sets?.[provider] || [])];
342
+ const rowOnly = {};
343
+ for (const provider of PROVIDERS) rowOnly[provider] = [...(sets?.rowOnly?.[provider] || [])];
344
+ sessionStorage.setItem(CATALOG_STORE_KEY, JSON.stringify({ catalog, hidden, rowOnly }));
345
+ } catch { /* full or unavailable */ }
346
+ }
347
+ (() => {
348
+ try {
349
+ if (typeof sessionStorage === 'undefined' || _catalog) return;
350
+ const kept = JSON.parse(sessionStorage.getItem(CATALOG_STORE_KEY) || 'null');
351
+ if (!kept?.catalog || typeof kept.catalog !== 'object') return;
352
+ _catalog = kept.catalog;
353
+ _catalogAt = 0;
354
+ const sets = { rowOnly: {} };
355
+ for (const provider of PROVIDERS) {
356
+ sets[provider] = new Set(Array.isArray(kept.hidden?.[provider]) ? kept.hidden[provider] : []);
357
+ sets.rowOnly[provider] = new Set(Array.isArray(kept.rowOnly?.[provider]) ? kept.rowOnly[provider] : []);
358
+ }
359
+ _hidden = sets;
360
+ } catch { /* unreadable: the first load builds it */ }
361
+ })();
362
+
329
363
  /** The last loadModelCatalog() result, synchronously (null before the first load). */
330
364
  export function cachedModelCatalog() { return _catalog; }
331
365
 
@@ -346,6 +380,8 @@ export function invalidateModelCatalog() {
346
380
  _catalogAt = 0;
347
381
  _catalogPartial = false;
348
382
  _catalogEpoch += 1;
383
+ // A reload must not paint the lists this replaced.
384
+ try { if (typeof sessionStorage !== 'undefined') sessionStorage.removeItem(CATALOG_STORE_KEY); } catch { /* unavailable */ }
349
385
  }
350
386
 
351
387
  /**
@@ -366,6 +402,15 @@ function modelListsChanged() {
366
402
  for (const listener of [..._catalogListeners]) {
367
403
  try { listener(); } catch { /* a listener's failure is its own */ }
368
404
  }
405
+ scheduleCatalogReload();
406
+ }
407
+
408
+ // In a page: the new lists are read right away, so the next menu opens from them (an open that comes
409
+ // first joins this read).
410
+ let _catalogReloadTimer = null;
411
+ function scheduleCatalogReload() {
412
+ if (typeof document === 'undefined' || _catalogReloadTimer) return;
413
+ _catalogReloadTimer = setTimeout(() => { _catalogReloadTimer = null; loadModelCatalog().catch(() => {}); }, 250);
369
414
  }
370
415
 
371
416
  /**
@@ -397,6 +442,12 @@ on('sync:opencode:hidden-models-changed', modelListsChanged);
397
442
  on('sync:assistant:hidden-models-changed', modelListsChanged);
398
443
  on('sync:assistant:catalog-changed', modelListsChanged);
399
444
 
445
+ // Launch: the menus' lists are read once the page has painted, so the first model menu opens from them.
446
+ if (typeof document !== 'undefined') {
447
+ registerLaunchPrefetch('assistant-model-catalog', () => loadModelCatalog(), { order: 10, owner: true });
448
+ registerLaunchPrefetch('assistant-accounts', () => Promise.allSettled([fetchClaudeAccounts(), fetchCodexAccounts(), fetchMcpProfilePresets(), fetchOpencodeAgents()]), { order: 14, owner: true });
449
+ }
450
+
400
451
  function sameModel(selected, m) {
401
452
  if (!selected) return false;
402
453
  return selected === m.id || selected === m.selector;
@@ -435,74 +486,28 @@ export async function openModelMenu(anchor, opts = {}) {
435
486
  if (!anchor) return null;
436
487
  const tr = tf(t);
437
488
  const b = normalizeBrain(brain);
438
- anchor.setAttribute('aria-busy', 'true');
439
- // Open immediately: a slow provider still has a visible, cancellable state.
440
- const loading = opts.routingEditor ? openMenu(anchor, {
441
- title, className: 'asst-model-menu asst-route-model-menu',
442
- items: [{ kind: 'info', label: tr('assistant.routes.editorState.loadingModels', 'Loading models…') }],
443
- width: 380, zIndex: opts.zIndex ?? null, repositionOnResize: true,
444
- }) : null;
445
- if (loading) loading.el.setAttribute('aria-busy', 'true');
446
- if (loading) { loading.el.setAttribute('role', 'dialog'); anchor.setAttribute('aria-haspopup', 'dialog'); }
489
+ // The kept catalog paints the menu at once (it is read again in the background below, and an open menu
490
+ // follows it in place); only a first open, or a reload the user asked for, waits for the lists.
491
+ const kept = opts.forceReload === true ? null : cachedModelCatalog();
447
492
  let catalog = {};
448
493
  let failed = false;
449
- try { catalog = await loadModelCatalog({ force: opts.forceReload === true }); } catch { failed = true; } finally { anchor.removeAttribute('aria-busy'); }
450
- if (!anchor.isConnected || (loading && currentMenu() !== loading.el)) return null;
451
- const metadata = modelCatalogStatus();
452
- const items = [];
453
- if (Array.isArray(opts.accountItems) && opts.accountItems.length) {
454
- items.push({ kind: 'header', label: opts.accountTitle || tr('assistant.brain.account', 'Account profile') }, ...opts.accountItems, { kind: 'separator' });
455
- }
456
- if (includeHere) {
457
- items.push({ kind: 'radio', id: '__here', label: hereLabel || tr('assistant.routes.brainDecides', 'Let the brain decide'), selected: !current, onSelect: () => onSelect?.({ here: true }) });
458
- }
459
- const list = Array.isArray(providers) && providers.length ? PROVIDERS.filter(p => providers.includes(p)) : PROVIDERS;
460
- let listed = 0;
461
- for (const provider of list) {
462
- const models = (catalog[provider] || []).filter(m => typeof accept !== 'function' || accept(provider, m));
463
- if (!models.length && !opts.routingEditor) continue;
464
- listed += models.length;
465
- const meta = getProviderMeta(provider);
466
- const hiddenCount = metadata.hiddenCounts[provider] || 0;
467
- 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') : '';
468
- items.push({ kind: 'header', label: meta.label, icon: meta.icon, color: meta.color, note });
469
- for (const m of models) {
470
- items.push({
471
- kind: 'radio',
472
- id: `${provider}:${m.id}`,
473
- label: m.label,
474
- desc: modelDesc(m, tr, opts.routingEditor),
475
- title: `${m.label || m.id} · ${m.id}`,
476
- tag: tierTag(m, tr),
477
- keywords: `${meta.label} ${m.id}`,
478
- selected: !!current && current.provider === provider && sameModel(current.model, m),
479
- 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 }),
480
- });
481
- }
482
- 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.') });
483
- }
484
- // Nothing passes the filter (e.g. no model makes video yet): say so instead of an empty list.
485
- if (!listed && typeof accept === 'function' && emptyLabel) items.push({ kind: 'info', label: emptyLabel });
486
- 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.') });
487
- if (allowCustom) {
488
- items.push({ kind: 'separator' });
489
- items.push({
490
- kind: 'input',
491
- placeholder: tr('assistant.brain.modelCustom', 'Custom model id…'),
492
- onSubmit: (v) => {
493
- const provider = current?.provider || b.provider;
494
- const disabled = hiddenModelFor(provider, v);
495
- if (disabled) {
496
- onToast?.(tr('assistant.brain.modelDisabled', "{model} is disabled in the Assistant's Models list.", { model: disabled }));
497
- return;
498
- }
499
- onSelect?.({ provider, model: v, label: modelShortName(v), custom: true });
500
- },
501
- });
502
- }
503
- if (!opts.routingEditor && typeof opts.onManageModels === 'function') {
504
- items.push({ id: '__manage_models', label: tr('assistant.models.manage', 'Manage models…'), onSelect: () => opts.onManageModels() });
494
+ let loading = null;
495
+ if (kept) {
496
+ catalog = kept;
497
+ } else {
498
+ anchor.setAttribute('aria-busy', 'true');
499
+ // Open immediately: a slow provider still has a visible, cancellable state.
500
+ loading = opts.routingEditor ? openMenu(anchor, {
501
+ title, className: 'asst-model-menu asst-route-model-menu',
502
+ items: [{ kind: 'info', label: tr('assistant.routes.editorState.loadingModels', 'Loading models…') }],
503
+ width: 380, zIndex: opts.zIndex ?? null, repositionOnResize: true,
504
+ }) : null;
505
+ if (loading) loading.el.setAttribute('aria-busy', 'true');
506
+ if (loading) { loading.el.setAttribute('role', 'dialog'); anchor.setAttribute('aria-haspopup', 'dialog'); }
507
+ try { catalog = await loadModelCatalog({ force: opts.forceReload === true }); } catch { failed = true; } finally { anchor.removeAttribute('aria-busy'); }
508
+ if (!anchor.isConnected || (loading && currentMenu() !== loading.el)) return null;
505
509
  }
510
+ const items = buildModelMenuItems(catalog);
506
511
  const menu = openMenu(anchor, {
507
512
  title: title || tr('assistant.brain.model', 'Model'),
508
513
  items,
@@ -516,7 +521,79 @@ export async function openModelMenu(anchor, opts = {}) {
516
521
  repositionOnResize: true,
517
522
  filterEmptyLabel: tr('assistant.routes.menu.noMatches', 'No matching models. Check Models for hidden choices.'),
518
523
  });
519
- if (opts.routingEditor && menu) {
524
+ if (opts.routingEditor && menu) decorateRouteMenu(menu, failed);
525
+ // Painted from the kept catalog: read it again (fresh within its minute, else a background request) and
526
+ // redraw the open menu in place when the lists changed, leaving the filter, the scroll and the focus alone.
527
+ if (kept && menu && !opts.routingEditor) {
528
+ loadModelCatalog().then((next) => {
529
+ if (!next || next === kept || currentMenu() !== menu.el || !anchor.isConnected) return;
530
+ const nextItems = buildModelMenuItems(next);
531
+ if (menuItemsSignature(nextItems) !== menuItemsSignature(items)) menu.update({ items: nextItems, keepPlace: true });
532
+ }).catch(() => { /* the kept list stays */ });
533
+ }
534
+ return menu;
535
+
536
+ function buildModelMenuItems(catalog) {
537
+ const metadata = modelCatalogStatus();
538
+ const items = [];
539
+ if (Array.isArray(opts.accountItems) && opts.accountItems.length) {
540
+ items.push({ kind: 'header', label: opts.accountTitle || tr('assistant.brain.account', 'Account profile') }, ...opts.accountItems, { kind: 'separator' });
541
+ }
542
+ if (includeHere) {
543
+ items.push({ kind: 'radio', id: '__here', label: hereLabel || tr('assistant.routes.brainDecides', 'Let the brain decide'), selected: !current, onSelect: () => onSelect?.({ here: true }) });
544
+ }
545
+ const list = Array.isArray(providers) && providers.length ? PROVIDERS.filter(p => providers.includes(p)) : PROVIDERS;
546
+ let listed = 0;
547
+ for (const provider of list) {
548
+ const models = (catalog[provider] || []).filter(m => typeof accept !== 'function' || accept(provider, m));
549
+ if (!models.length && !opts.routingEditor) continue;
550
+ listed += models.length;
551
+ const meta = getProviderMeta(provider);
552
+ const hiddenCount = metadata.hiddenCounts[provider] || 0;
553
+ 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') : '';
554
+ items.push({ kind: 'header', label: meta.label, icon: meta.icon, color: meta.color, note });
555
+ for (const m of models) {
556
+ items.push({
557
+ kind: 'radio',
558
+ id: `${provider}:${m.id}`,
559
+ label: m.label,
560
+ desc: modelDesc(m, tr, opts.routingEditor),
561
+ title: `${m.label || m.id} · ${m.id}`,
562
+ tag: tierTag(m, tr),
563
+ keywords: `${meta.label} ${m.id}`,
564
+ selected: !!current && current.provider === provider && sameModel(current.model, m),
565
+ 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 }),
566
+ });
567
+ }
568
+ 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.') });
569
+ }
570
+ // Nothing passes the filter (e.g. no model makes video yet): say so instead of an empty list.
571
+ if (!listed && typeof accept === 'function' && emptyLabel) items.push({ kind: 'info', label: emptyLabel });
572
+ 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.') });
573
+ if (allowCustom) {
574
+ items.push({ kind: 'separator' });
575
+ items.push({
576
+ kind: 'input',
577
+ placeholder: tr('assistant.brain.modelCustom', 'Custom model id…'),
578
+ onSubmit: (v) => {
579
+ const provider = current?.provider || b.provider;
580
+ const disabled = hiddenModelFor(provider, v);
581
+ if (disabled) {
582
+ onToast?.(tr('assistant.brain.modelDisabled', "{model} is disabled in the Assistant's Models list.", { model: disabled }));
583
+ return;
584
+ }
585
+ onSelect?.({ provider, model: v, label: modelShortName(v), custom: true });
586
+ },
587
+ });
588
+ }
589
+ if (!opts.routingEditor && typeof opts.onManageModels === 'function') {
590
+ items.push({ id: '__manage_models', label: tr('assistant.models.manage', 'Manage models…'), onSelect: () => opts.onManageModels() });
591
+ }
592
+ return items;
593
+ }
594
+
595
+ function decorateRouteMenu(menu, failed) {
596
+ const metadata = modelCatalogStatus();
520
597
  // Search and a custom-id input belong to a dialog; choices are a radio group.
521
598
  // A menu role cannot contain these form fields or recovery buttons.
522
599
  menu.el.setAttribute('role', 'dialog');
@@ -546,7 +623,11 @@ export async function openModelMenu(anchor, opts = {}) {
546
623
  }
547
624
  menu.el.append(footer); menu.reposition();
548
625
  }
549
- return menu;
626
+ }
627
+
628
+ /** What a model menu lists, for telling a fresher read from the one on screen. */
629
+ function menuItemsSignature(items) {
630
+ return JSON.stringify((items || []).map((item) => [item.kind || '', item.id ?? '', item.label || '', item.desc || '', item.tag || '', item.note || '', !!item.selected]));
550
631
  }
551
632
 
552
633
  // ── Picker ──────────────────────────────────────────────────────────────────
@@ -968,6 +1049,7 @@ export function createBrainPicker(host, hooks = {}) {
968
1049
  }).catch(() => { _projectsCache = _projectsCache || []; }));
969
1050
  }
970
1051
  await Promise.allSettled(jobs);
1052
+ if (cachedModelCatalog()) _warmProviders.add(provider);
971
1053
  }
972
1054
 
973
1055
  async function refresh(force = false) {
@@ -999,7 +1081,11 @@ export function createBrainPicker(host, hooks = {}) {
999
1081
  const anchor = anchorFor(field, anchorOverride);
1000
1082
  const brain = currentBrain();
1001
1083
  if (field === 'model') {
1002
- if (ACCOUNT_PROVIDERS.has(brain.provider)) await ensureLoaded(brain.provider);
1084
+ // The account rows come from the kept lists; they are read again in the background once loaded.
1085
+ if (ACCOUNT_PROVIDERS.has(brain.provider)) {
1086
+ if (_warmProviders.has(brain.provider)) ensureLoaded(brain.provider).catch(() => {});
1087
+ else await ensureLoaded(brain.provider);
1088
+ }
1003
1089
  await openModelMenu(anchor, {
1004
1090
  brain,
1005
1091
  t: hooks.t,
@@ -1034,12 +1120,48 @@ export function createBrainPicker(host, hooks = {}) {
1034
1120
  });
1035
1121
  return;
1036
1122
  }
1037
- // Everything else needs the provider catalog — load it lazily on first open.
1038
- anchor.setAttribute('aria-busy', 'true');
1039
- try { await ensureLoaded(brain.provider); } finally { anchor.removeAttribute('aria-busy'); }
1040
- if (destroyed || !anchor.isConnected) return;
1041
- const menu = (title, items, extra = {}) => openMenu(anchor, { title, items, placement, role: 'menu', ...extra });
1123
+ // The account menu reads the kept rows and asks again by itself (asst-account-menu.js).
1124
+ if (field === 'account') {
1125
+ if (!ACCOUNT_PROVIDERS.has(brain.provider)) return;
1126
+ if (!_warmProviders.has(brain.provider)) {
1127
+ anchor.setAttribute('aria-busy', 'true');
1128
+ try { await ensureLoaded(brain.provider); } finally { anchor.removeAttribute('aria-busy'); }
1129
+ if (destroyed || !anchor.isConnected) return;
1130
+ } else {
1131
+ ensureLoaded(brain.provider).catch(() => {});
1132
+ }
1133
+ // Kept inside the panel that holds the anchor (the sidepanel, or the tab's own root).
1134
+ openAccountMenu(anchor, accountMenu(brain, anchor.closest?.('.assistant-panel') || anchor.closest?.('.asst-root') || null));
1135
+ return;
1136
+ }
1137
+ // Everything else needs the provider catalog — load it lazily on first open. Once it is loaded the menu
1138
+ // opens from what is kept, the lists are read again in the background, and an open menu they change is
1139
+ // redrawn in place (the filter, the scroll and the focus stay).
1140
+ const warm = _warmProviders.has(brain.provider);
1141
+ if (!warm) {
1142
+ anchor.setAttribute('aria-busy', 'true');
1143
+ try { await ensureLoaded(brain.provider); } finally { anchor.removeAttribute('aria-busy'); }
1144
+ if (destroyed || !anchor.isConnected) return;
1145
+ }
1146
+ let shown = null;
1147
+ drawField(field, brain, (title, items, extra = {}) => {
1148
+ const opened = openMenu(anchor, { title, items, placement, role: 'menu', ...extra });
1149
+ shown = opened ? { opened, items } : null;
1150
+ return opened;
1151
+ }, { anchor, restoreFocus });
1152
+ if (warm && shown) {
1153
+ ensureLoaded(brain.provider).then(() => {
1154
+ if (destroyed || !shown || currentMenu() !== shown.opened.el || !anchor.isConnected) return;
1155
+ let fresh = null;
1156
+ drawField(field, currentBrain(), (_title, items) => { fresh = items; return null; }, { anchor, restoreFocus });
1157
+ if (fresh && menuItemsSignature(fresh) !== menuItemsSignature(shown.items)) shown.opened.update({ items: fresh, keepPlace: true });
1158
+ }).catch(() => {});
1159
+ }
1160
+ }
1042
1161
 
1162
+ // The menu of one field, from the lists as they are kept now: `menu(title, items, extra)` opens it (or, for a
1163
+ // redraw, only takes the items).
1164
+ function drawField(field, brain, menu, { anchor, restoreFocus = false } = {}) {
1043
1165
  if (field === 'effort') {
1044
1166
  if (brain.provider === 'opencode') {
1045
1167
  const agents = getCachedOpencodeAgents();
@@ -1070,13 +1192,6 @@ export function createBrainPicker(host, hooks = {}) {
1070
1192
  return;
1071
1193
  }
1072
1194
 
1073
- if (field === 'account') {
1074
- if (!ACCOUNT_PROVIDERS.has(brain.provider)) return;
1075
- // Kept inside the panel that holds the anchor (the sidepanel, or the tab's own root).
1076
- openAccountMenu(anchor, accountMenu(brain, anchor.closest?.('.assistant-panel') || anchor.closest?.('.asst-root') || null));
1077
- return;
1078
- }
1079
-
1080
1195
  if (field === 'project') {
1081
1196
  const actor = anchor === heroProject ? heroProjectMascot : null;
1082
1197
  const pick = project => {
@@ -204,7 +204,7 @@ export function createComposer(hostEl, hooks = {}) {
204
204
  function renderAttachments() {
205
205
  const chips = [];
206
206
  state.images.forEach((img, i) => {
207
- chips.push(`<span class="asst-chip static" data-kind="image" data-idx="${i}"><img src="data:${esc(img.mediaType)};base64,${img.base64}" alt=""><span class="asst-chip-text">${esc(img.name || 'image')}</span><button type="button" class="asst-chip-x" aria-label="${esc(t('common.remove', 'Remove'))}">${ICON_X}</button></span>`);
207
+ chips.push(`<span class="asst-chip static" data-kind="image" data-idx="${i}"><img src="data:${esc(img.mediaType)};base64,${esc(img.base64)}" alt=""><span class="asst-chip-text">${esc(img.name || 'image')}</span><button type="button" class="asst-chip-x" aria-label="${esc(t('common.remove', 'Remove'))}">${ICON_X}</button></span>`);
208
208
  });
209
209
  state.files.forEach((f, i) => {
210
210
  chips.push(`<span class="asst-chip static" data-kind="file" data-idx="${i}"><span class="asst-chip-text">${esc(f.name)}</span><span class="asst-chip-meta">${esc(Math.ceil(f.content.length / 1024))}k</span><button type="button" class="asst-chip-x" aria-label="${esc(t('common.remove', 'Remove'))}">${ICON_X}</button></span>`);
@@ -54,7 +54,7 @@ export function describeBrainFallback(fallback) {
54
54
  }
55
55
 
56
56
  /**
57
- * deps: { api: { list(), get(id), create({brain,label,fallback}) },
57
+ * deps: { api: { list(), get(id), create({brain,label,fallback,draft}), remove?(id) },
58
58
  * isBlocked() → bool, toast(text), storedBrain() → brain|null,
59
59
  * fallbackText(session.fallback) → text, defaultHost: 'terminal' }
60
60
  */
@@ -149,7 +149,9 @@ export function createAssistantHosts({
149
149
  try {
150
150
  // A model disabled since it was picked (Assistant → Models) must not
151
151
  // lock the Assistant out: the server starts on its enabled stand-in.
152
- res = await api.create({ brain: b, label: label || undefined, fallback: true });
152
+ // A draft: the server stores and lists it from its first prompt on (a prompt
153
+ // given here is that prompt); closed or left unused, nothing of it remains.
154
+ res = await api.create({ brain: b, label: label || undefined, fallback: true, draft: true });
153
155
  } catch (err) {
154
156
  say(`Assistant unavailable: ${err?.message || err}`);
155
157
  return null;
@@ -245,6 +247,48 @@ export function createAssistantHosts({
245
247
  return true;
246
248
  }
247
249
 
250
+ /**
251
+ * A session nobody chatted in that the user left (they picked another one in its Sessions menu):
252
+ * its tab closes the way the user closes one, and the server removes it (it holds no conversation).
253
+ */
254
+ function discard(sessionId) {
255
+ const owner = ownerOf(sessionId);
256
+ if (last?.sessionId === sessionId) last = null;
257
+ if (!owner) return false;
258
+ adapters.get(owner).close(sessionId, { closeServer: true });
259
+ return true;
260
+ }
261
+
262
+ /**
263
+ * The Sessions menu's Remove: the server stops the session's running turn and workers and purges it,
264
+ * then its tab closes wherever it is open (other windows: the session-ended broadcast). `fresh`: it is
265
+ * the session showing where the user asked, and the host that has it gets a new one first, as New does
266
+ * (a host is never left empty in between). → { ok: true, stoppedRuns } | { ok: false, error }
267
+ */
268
+ async function remove(sessionId, { host = null, fresh = false, brain = null } = {}) {
269
+ if (!sessionId || !api.remove) return { ok: false, error: 'unavailable' };
270
+ if (isBlocked()) return { ok: false, error: 'blocked' };
271
+ if (fresh) { try { await open({ host: host || ownerOf(sessionId), brain }); } catch { /* the removal goes on */ } }
272
+ let res = null;
273
+ try { res = await api.remove(sessionId); }
274
+ catch (err) { return { ok: false, error: err?.message || String(err) }; }
275
+ ended(sessionId);
276
+ return { ok: true, stoppedRuns: Array.isArray(res?.stoppedRuns) ? res.stoppedRuns : [] };
277
+ }
278
+
279
+ /**
280
+ * The server no longer has a session a host shows (a draft it dropped, a restart before its first
281
+ * prompt, a removal this page missed): its tab closes, and with `fresh` (it was showing and had
282
+ * no conversation) a new session takes its place in the host that had it, so the user never sees a dead tab.
283
+ */
284
+ async function gone(sessionId, { host = null, fresh = false, brain = null } = {}) {
285
+ if (!sessionId) return null;
286
+ let opened = null;
287
+ if (fresh && !isBlocked()) { try { opened = await open({ host: host || ownerOf(sessionId), brain }); } catch { opened = null; } }
288
+ ended(sessionId);
289
+ return opened;
290
+ }
291
+
248
292
  return {
249
293
  register,
250
294
  ownerOf,
@@ -253,6 +297,9 @@ export function createAssistantHosts({
253
297
  toggle,
254
298
  show,
255
299
  ended,
300
+ discard,
301
+ remove,
302
+ gone,
256
303
  noteFocus,
257
304
  fetchLive,
258
305
  isLive: isAssistantSessionLive,
@@ -31,6 +31,10 @@
31
31
  // tag { label } → non-interactive badge row
32
32
  // input { placeholder, value, onSubmit } → inline text field
33
33
  // buttons { label, buttons: [item…] } → one row of compact items (a group)
34
+ // `action: { icon, label, tooltip, id, onSelect }` on an item (or radio / check) adds one icon button at the
35
+ // row's end, shown on hover and keyboard focus (the Sessions menu's Remove). It is not a stop of ↑/↓: → from
36
+ // its row reaches it, ← goes back, Delete on the row presses it; choosing it never chooses the row and leaves
37
+ // the menu open (its onSelect usually redraws it with update()).
34
38
  // `render(button, { checked })` on an item draws its content instead of label/desc/tag.
35
39
  // `className` on an item or an info row adds its own class.
36
40
  // `keepOpen` on an item leaves the menu open when it is chosen.
@@ -38,7 +42,7 @@
38
42
  // Keys: ↑/↓ move, Home/End jump, Enter/Space activate, Esc closes (focus back
39
43
  // to the anchor), Tab closes. The optional filter hides non-matching items and
40
44
  // 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).
45
+ // an open menu in place (focus: the data-id to land on; the filter's text still applies).
42
46
 
43
47
  let _open = null;
44
48
  let _uid = 0;
@@ -101,6 +105,11 @@ export function currentMenu() {
101
105
  return _open?.menu || null;
102
106
  }
103
107
 
108
+ /** The element the open menu belongs to (null when none is open). */
109
+ export function currentMenuAnchor() {
110
+ return _open?.anchor || null;
111
+ }
112
+
104
113
  function itemButton(item, kind, { role, list, onActivate }) {
105
114
  const btn = el('button', `asst-dd-item${item.danger ? ' danger' : ''}${item.className ? ` ${item.className}` : ''}`);
106
115
  btn.type = 'button';
@@ -145,12 +154,28 @@ function itemButton(item, kind, { role, list, onActivate }) {
145
154
  return btn;
146
155
  }
147
156
 
157
+ /** A row's own action (item.action): an icon button beside the row, outside the ↑/↓ cycle. */
158
+ function actionButton(action, item, { role }) {
159
+ const btn = el('button', `asst-iconbtn asst-dd-item-action${action.className ? ` ${action.className}` : ''}`);
160
+ btn.type = 'button';
161
+ btn.tabIndex = -1;
162
+ btn.setAttribute('role', role === 'listbox' ? 'option' : 'menuitem');
163
+ if (action.id != null) btn.dataset.id = String(action.id);
164
+ const label = String(action.label || '');
165
+ if (label) btn.setAttribute('aria-label', label);
166
+ if (action.tooltip || label) btn.dataset.tooltip = action.tooltip || label;
167
+ btn.innerHTML = action.icon || '';
168
+ btn.querySelector('svg')?.setAttribute('aria-hidden', 'true');
169
+ btn.addEventListener('click', (e) => { e.stopPropagation(); action.onSelect?.(item, btn); });
170
+ return btn;
171
+ }
172
+
148
173
  function buildItems(list, items, { role, onActivate }) {
149
174
  for (const item of items || []) {
150
175
  const kind = itemKind(item);
151
176
  if (kind === 'separator') { list.appendChild(el('div', 'asst-dd-sep')).setAttribute('role', 'separator'); continue; }
152
177
  if (kind === 'buttons') {
153
- const row = el('div', 'asst-dd-btnrow');
178
+ const row = el('div', `asst-dd-btnrow${item.className ? ` ${item.className}` : ''}`);
154
179
  row.setAttribute('role', 'group');
155
180
  if (item.id != null) row.dataset.id = String(item.id);
156
181
  if (item.ariaLabel || item.label) row.setAttribute('aria-label', item.ariaLabel || item.label);
@@ -188,6 +213,14 @@ function buildItems(list, items, { role, onActivate }) {
188
213
  list.appendChild(input);
189
214
  continue;
190
215
  }
216
+ if (item.action && typeof item.action === 'object' && !item.disabled) {
217
+ const row = el('div', 'asst-dd-itemrow');
218
+ row.setAttribute('role', 'none');
219
+ row.appendChild(itemButton(item, kind, { role, list, onActivate }));
220
+ row.appendChild(actionButton(item.action, item, { role }));
221
+ list.appendChild(row);
222
+ continue;
223
+ }
191
224
  list.appendChild(itemButton(item, kind, { role, list, onActivate }));
192
225
  }
193
226
  if (!list.querySelector('.asst-dd-item, .asst-dd-input, .asst-dd-info, .asst-dd-tagrow')) list.appendChild(el('div', 'asst-dd-empty', '—'));
@@ -199,6 +232,8 @@ export function applyMenuFilter(list, query, emptyLabel = 'No matches') {
199
232
  const rows = [...list.children];
200
233
  for (const row of rows) {
201
234
  if (row.classList.contains('asst-dd-item')) row.hidden = !!q && !row.dataset.search.includes(q);
235
+ // A row with its own action: shown while its item matches.
236
+ else if (row.classList.contains('asst-dd-itemrow')) row.hidden = !!q && !(row.querySelector('.asst-dd-item')?.dataset.search || '').includes(q);
202
237
  // A button row stays whole: shown while one of its buttons matches.
203
238
  else if (row.classList.contains('asst-dd-btnrow')) row.hidden = !!q && ![...row.querySelectorAll('.asst-dd-item')].some(b => b.dataset.search.includes(q));
204
239
  else if (row.classList.contains('asst-dd-info') || row.classList.contains('asst-dd-tagrow')) row.hidden = !!q;
@@ -226,7 +261,7 @@ export function applyMenuFilter(list, query, emptyLabel = 'No matches') {
226
261
  const last = visible[visible.length - 1];
227
262
  if (last?.classList.contains('asst-dd-sep')) last.hidden = true;
228
263
  let empty = list.querySelector('.asst-dd-empty.asst-dd-noresults');
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')));
264
+ const hasItem = rows.some(r => !r.hidden && (r.classList.contains('asst-dd-item') || r.classList.contains('asst-dd-itemrow') || r.classList.contains('asst-dd-btnrow') || r.classList.contains('asst-dd-input')));
230
265
  if (q && !hasItem) {
231
266
  if (!empty) { empty = el('div', 'asst-dd-empty asst-dd-noresults', emptyLabel); list.appendChild(empty); }
232
267
  empty.hidden = false;
@@ -422,7 +457,9 @@ export function openMenu(anchor, opts = {}) {
422
457
  const target = e.target;
423
458
  const inInput = target?.classList?.contains('asst-dd-input');
424
459
  const nodes = focusables(menu);
425
- const idx = nodes.indexOf(document.activeElement);
460
+ // On a row's action (item.action) the keys move from its row.
461
+ const onAction = document.activeElement?.classList?.contains('asst-dd-item-action') && menu.contains(document.activeElement) ? document.activeElement : null;
462
+ const idx = nodes.indexOf(onAction ? onAction.previousElementSibling : document.activeElement);
426
463
  if (e.key === 'Escape') {
427
464
  e.preventDefault(); e.stopPropagation();
428
465
  if (typeof opts.onEscape === 'function' && opts.onEscape() === true) return;
@@ -447,6 +484,18 @@ export function openMenu(anchor, opts = {}) {
447
484
  setFocused(menu, nodes[next]);
448
485
  return;
449
486
  }
487
+ if (!inInput && (e.key === 'ArrowRight' || e.key === 'ArrowLeft' || e.key === 'Delete')) {
488
+ const row = onAction ? null : document.activeElement?.closest?.('.asst-dd-item');
489
+ const action = onAction || (row?.parentElement?.classList.contains('asst-dd-itemrow') ? row.nextElementSibling : null);
490
+ if (action?.classList.contains('asst-dd-item-action')) {
491
+ e.preventDefault(); e.stopPropagation();
492
+ const main = action.previousElementSibling;
493
+ if (e.key === 'ArrowLeft') { if (onAction) setFocused(menu, main); return; }
494
+ if (!onAction) { setFocused(menu, main); try { action.focus({ preventScroll: true }); } catch { action.focus(); } }
495
+ if (e.key === 'Delete') action.click();
496
+ return;
497
+ }
498
+ }
450
499
  if ((e.key === 'Home' || e.key === 'End') && !inInput) {
451
500
  e.preventDefault(); e.stopPropagation();
452
501
  if (nodes.length) setFocused(menu, nodes[e.key === 'Home' ? 0 : nodes.length - 1]);
@@ -464,6 +513,7 @@ export function openMenu(anchor, opts = {}) {
464
513
  return;
465
514
  }
466
515
  if ((e.key === 'Enter' || e.key === ' ') && !inInput) {
516
+ if (onAction) { e.preventDefault(); e.stopPropagation(); onAction.click(); return; }
467
517
  const current = document.activeElement?.closest?.('.asst-dd-item');
468
518
  if (current && menu.contains(current)) { e.preventDefault(); e.stopPropagation(); current.click(); }
469
519
  return;
@@ -499,14 +549,35 @@ export function openMenu(anchor, opts = {}) {
499
549
  setFocused(menu, selected || first || null);
500
550
  }
501
551
  // Redraw in place (another view of the same menu): the anchor, the listeners and the focus stay in it.
552
+ // `keepPlace` (fresher data for the same view, read in the background): the scroll position, the row the
553
+ // keyboard is on and the filter's text and focus are left where the user had them.
502
554
  const update = (next = {}) => {
503
555
  if (!_open || _open.menu !== menu) return;
556
+ const keep = next.keepPlace === true;
557
+ const scrollBefore = list.scrollTop;
558
+ const active = document.activeElement;
559
+ const typing = !!active && active.classList?.contains('asst-dd-input') && menu.contains(active);
560
+ const onRowId = !typing ? (menu.querySelector('.asst-dd-item.focused')?.dataset?.id ?? null) : null;
504
561
  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; }
562
+ if (Array.isArray(next.items)) {
563
+ list.replaceChildren();
564
+ buildItems(list, next.items, { role, onActivate: activate });
565
+ if (filterInput?.value) applyMenuFilter(list, filterInput.value, opts.filterEmptyLabel);
566
+ list.scrollTop = keep ? scrollBefore : 0;
567
+ }
506
568
  if ('footer' in next) fillFooter(next.footer);
507
569
  place();
508
- const wanted = next.focus != null ? [...menu.querySelectorAll('.asst-dd-item')].find(n => n.dataset.id === String(next.focus) && !n.disabled) : null;
570
+ if (keep && typing) {
571
+ // The input keeps the caret; the row highlight follows the selection as it did when it opened.
572
+ menu.querySelectorAll('.asst-dd-item.focused').forEach(n => n.classList.remove('focused'));
573
+ list.querySelector('.asst-dd-item[data-checked="1"]:not([disabled])')?.classList.add('focused');
574
+ return;
575
+ }
576
+ const keptRow = keep && onRowId != null ? [...menu.querySelectorAll('.asst-dd-item')].find(n => n.dataset.id === onRowId && !n.disabled) : null;
577
+ const wanted = keptRow || (next.focus != null ? [...menu.querySelectorAll('.asst-dd-item')].find(n => n.dataset.id === String(next.focus) && !n.disabled) : null);
578
+ if (keep && !keptRow && !menu.contains(document.activeElement)) return; // focus is elsewhere: not taken back
509
579
  setFocused(menu, wanted || list.querySelector('.asst-dd-item[data-checked="1"]:not([disabled])') || focusables(menu)[0] || null);
580
+ if (keep) list.scrollTop = scrollBefore;
510
581
  };
511
582
  return { el: menu, list, close: closeMenu, reposition: place, update };
512
583
  }