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
@@ -0,0 +1,331 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — Code editor: the tabs, as a model
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // Which documents the editor panel holds, in which order, which one is shown,
6
+ // and what an open or a close does to that: a file that is already open is
7
+ // shown again, a tab nobody edited or pinned is reused by the next file, a
8
+ // closed tab gives its place to the one used before it. Also what is kept of
9
+ // the tabs per project, and what comes back from it. editor-panel.js holds the
10
+ // documents themselves (their text, history and file facts) by tab id;
11
+ // editor-tabs.js draws the strip. No DOM and no imports: loaded by
12
+ // `node --test` as it is.
13
+
14
+ /** Tabs kept per project, and projects kept at all. */
15
+ export const MAX_REMEMBERED_TABS = 20;
16
+ export const MAX_REMEMBERED_PROJECTS = 30;
17
+ export const REMEMBERED_VERSION = 1;
18
+
19
+ /**
20
+ * @typedef {object} EditorTab
21
+ * @property {string} id unique for the page
22
+ * @property {string} key what it is a tab of: a file by its path, a plan or a changelog by its identity too
23
+ * @property {'file' | 'plan' | 'changelog'} kind
24
+ * @property {string} path
25
+ * @property {string} name the file's name
26
+ * @property {boolean} pinned false while the next file opened may take its place
27
+ */
28
+
29
+ /**
30
+ * @typedef {object} EditorTabs
31
+ * @property {EditorTab[]} tabs in strip order
32
+ * @property {string | null} activeId
33
+ * @property {string[]} recent ids, the most recently shown first
34
+ * @property {number} seq
35
+ */
36
+
37
+ /** A path with forward slashes, the way the server answers it. */
38
+ export function normalizePath(path) {
39
+ return String(path ?? '').replace(/\\/g, '/');
40
+ }
41
+
42
+ /** The last part of a path. */
43
+ export function fileName(path) {
44
+ const text = normalizePath(path);
45
+ return text.slice(text.lastIndexOf('/') + 1);
46
+ }
47
+
48
+ /**
49
+ * What a tab is a tab of. Two requests with the same key are the same tab.
50
+ * @param {{ kind?: 'file' | 'plan' | 'changelog', path: string, identity?: string }} of `identity` tells one plan
51
+ * of a file from another (the panel that asked, the plan, the revision)
52
+ */
53
+ export function tabKey({ kind = 'file', path, identity = '' } = {}) {
54
+ const where = normalizePath(path);
55
+ return kind === 'file' ? `file:${where}` : `${kind}:${identity}:${where}`;
56
+ }
57
+
58
+ /** @returns {EditorTabs} no tab open */
59
+ export function createTabs() {
60
+ return { tabs: [], activeId: null, recent: [], seq: 0 };
61
+ }
62
+
63
+ export const getTab = (model, id) => model.tabs.find((tab) => tab.id === id) || null;
64
+ export const findTab = (model, key) => model.tabs.find((tab) => tab.key === key) || null;
65
+ export const tabIndex = (model, id) => model.tabs.findIndex((tab) => tab.id === id);
66
+ export const activeTab = (model) => getTab(model, model.activeId);
67
+
68
+ const shown = (recent, id) => [id, ...recent.filter((other) => other !== id)];
69
+
70
+ /** Show a tab. An id that is not open changes nothing. */
71
+ export function activateTab(model, id) {
72
+ if (!getTab(model, id)) return model;
73
+ if (model.activeId === id && model.recent[0] === id) return model;
74
+ return { ...model, activeId: id, recent: shown(model.recent, id) };
75
+ }
76
+
77
+ /**
78
+ * Open something in a tab and show it.
79
+ * - Already open (same key): that tab is shown; `pin` pins it.
80
+ * - A tab that was never pinned is open: the new document takes its place, under a new id.
81
+ * - Otherwise a tab is added to the right of the one that is shown.
82
+ * A plan or a changelog is pinned from the start; a file is pinned when `pin` says so.
83
+ * @param {EditorTabs} model
84
+ * @param {{ key: string, kind?: 'file' | 'plan' | 'changelog', path: string, name?: string, pin?: boolean }} request
85
+ * @param {{ canReuse?: (tab: EditorTab) => boolean }} [o] a tab that may not be given up (unsaved text, a save
86
+ * under way) is not reused even when it was never pinned
87
+ * @returns {{ model: EditorTabs, id: string, outcome: 'activated' | 'replaced' | 'added', replacedId: string | null }}
88
+ */
89
+ export function openTab(model, { key, kind = 'file', path, name, pin = false }, { canReuse = () => true } = {}) {
90
+ const existing = findTab(model, key);
91
+ if (existing) {
92
+ const tabs = pin && !existing.pinned ? model.tabs.map((tab) => (tab.id === existing.id ? { ...tab, pinned: true } : tab)) : model.tabs;
93
+ return { model: activateTab({ ...model, tabs }, existing.id), id: existing.id, outcome: 'activated', replacedId: null };
94
+ }
95
+ const seq = model.seq + 1;
96
+ const tab = { id: `tab-${seq}`, key, kind, path: normalizePath(path), name: name || fileName(path), pinned: !!pin || kind !== 'file' };
97
+ const reusable = model.tabs.find((other) => !other.pinned && canReuse(other));
98
+ if (reusable) {
99
+ const tabs = model.tabs.map((other) => (other.id === reusable.id ? tab : other));
100
+ const recent = shown(model.recent.filter((id) => id !== reusable.id), tab.id);
101
+ return { model: { tabs, activeId: tab.id, recent, seq }, id: tab.id, outcome: 'replaced', replacedId: reusable.id };
102
+ }
103
+ const at = tabIndex(model, model.activeId);
104
+ const tabs = [...model.tabs];
105
+ tabs.splice(at === -1 ? tabs.length : at + 1, 0, tab);
106
+ return { model: { tabs, activeId: tab.id, recent: shown(model.recent, tab.id), seq }, id: tab.id, outcome: 'added', replacedId: null };
107
+ }
108
+
109
+ /** Keep a tab: the next file opened no longer takes its place. */
110
+ export function pinTab(model, id) {
111
+ const tab = getTab(model, id);
112
+ if (!tab || tab.pinned) return model;
113
+ return { ...model, tabs: model.tabs.map((other) => (other.id === id ? { ...other, pinned: true } : other)) };
114
+ }
115
+
116
+ /** Change what a tab says about itself (the path the server resolved, its name). The id and the order stay. */
117
+ export function updateTab(model, id, patch) {
118
+ if (!getTab(model, id)) return model;
119
+ return { ...model, tabs: model.tabs.map((tab) => (tab.id === id ? { ...tab, ...patch, id: tab.id } : tab)) };
120
+ }
121
+
122
+ /**
123
+ * Close a tab. When it was the one shown, the tab used before it is shown; when none was, its neighbour.
124
+ * @returns {{ model: EditorTabs, activeId: string | null }}
125
+ */
126
+ export function closeTab(model, id) {
127
+ const at = tabIndex(model, id);
128
+ if (at === -1) return { model, activeId: model.activeId };
129
+ const tabs = model.tabs.filter((tab) => tab.id !== id);
130
+ const recent = model.recent.filter((other) => other !== id);
131
+ let activeId = model.activeId;
132
+ if (activeId === id) activeId = recent.find((other) => tabs.some((tab) => tab.id === other)) || tabs[Math.min(at, tabs.length - 1)]?.id || null;
133
+ const next = { ...model, tabs, activeId, recent: activeId ? shown(recent, activeId) : recent };
134
+ return { model: next, activeId };
135
+ }
136
+
137
+ /**
138
+ * The tabs a "close" command means.
139
+ * @param {EditorTabs} model
140
+ * @param {'one' | 'others' | 'saved' | 'all'} scope
141
+ * @param {string | null} id the tab the command was given on
142
+ * @param {(tab: EditorTab) => boolean} [isDirty] has unsaved changes ("saved" leaves those open)
143
+ * @returns {string[]} ids, in strip order
144
+ */
145
+ export function closeTargets(model, scope, id, isDirty = () => false) {
146
+ if (scope === 'one') return getTab(model, id) ? [id] : [];
147
+ if (scope === 'others') return model.tabs.filter((tab) => tab.id !== id).map((tab) => tab.id);
148
+ if (scope === 'saved') return model.tabs.filter((tab) => !isDirty(tab)).map((tab) => tab.id);
149
+ return model.tabs.map((tab) => tab.id);
150
+ }
151
+
152
+ /** Put a tab at another place in the strip (`index` is its place afterwards). */
153
+ export function moveTab(model, id, index) {
154
+ const from = tabIndex(model, id);
155
+ if (from === -1) return model;
156
+ const tabs = model.tabs.filter((tab) => tab.id !== id);
157
+ const to = Math.max(0, Math.min(tabs.length, Math.floor(Number(index)) || 0));
158
+ if (to === from) return model;
159
+ tabs.splice(to, 0, model.tabs[from]);
160
+ return { ...model, tabs };
161
+ }
162
+
163
+ /** The tab `delta` places from another in the strip, going round at the ends. Null when there is no other. */
164
+ export function neighborTab(model, id, delta) {
165
+ const at = tabIndex(model, id);
166
+ if (at === -1 || model.tabs.length < 2) return null;
167
+ const count = model.tabs.length;
168
+ return model.tabs[(((at + delta) % count) + count) % count].id;
169
+ }
170
+
171
+ /**
172
+ * What each tab shows as its name. Two files with the same name also show the folders that tell them apart.
173
+ * @param {EditorTab[]} tabs
174
+ * @returns {Map<string, { name: string, hint: string }>} by tab id; `hint` is '' for a name that is the only one
175
+ */
176
+ export function tabLabels(tabs) {
177
+ const labels = new Map(tabs.map((tab) => [tab.id, { name: tab.name, hint: '' }]));
178
+ const byName = new Map();
179
+ for (const tab of tabs) byName.set(tab.name, [...(byName.get(tab.name) || []), tab]);
180
+ for (const same of byName.values()) {
181
+ if (same.length < 2) continue;
182
+ const folders = same.map((tab) => normalizePath(tab.path).split('/').slice(0, -1).filter(Boolean));
183
+ const deepest = Math.max(...folders.map((parts) => parts.length));
184
+ let depth = 1;
185
+ const hintAt = (parts, n) => parts.slice(-n).join('/');
186
+ while (depth < deepest && new Set(folders.map((parts) => hintAt(parts, depth))).size < same.length) depth++;
187
+ same.forEach((tab, i) => { labels.set(tab.id, { name: tab.name, hint: hintAt(folders[i], depth) }); });
188
+ }
189
+ return labels;
190
+ }
191
+
192
+ // ── What is kept of the tabs ──
193
+
194
+ const position = (value) => (Number.isInteger(value) && value >= 0 ? value : 0);
195
+
196
+ /**
197
+ * The open files as they are kept for a project: their order, the one shown, and for each where the caret and
198
+ * the top of the view were. Plans and changelogs are not kept: each belongs to the panel that asked for it.
199
+ * @param {EditorTabs} model
200
+ * @param {(tab: EditorTab) => ({ anchor?: number, head?: number, top?: number, dirty?: boolean } | null)} describe
201
+ * where a tab is; null leaves the tab out (a file that could not be opened)
202
+ * @returns {{ active: string | null, tabs: { path: string, pinned: boolean, anchor: number, head: number, top: number, dirty: boolean }[] }}
203
+ */
204
+ export function serializeTabs(model, describe = () => ({})) {
205
+ let kept = [];
206
+ for (const tab of model.tabs) {
207
+ if (tab.kind !== 'file') continue;
208
+ const at = describe(tab);
209
+ if (!at) continue;
210
+ kept.push({ id: tab.id, path: tab.path, pinned: !!tab.pinned, anchor: position(at.anchor), head: position(at.head), top: position(at.top), dirty: !!at.dirty });
211
+ }
212
+ if (kept.length > MAX_REMEMBERED_TABS) {
213
+ // The ones used last stay, in strip order.
214
+ const rank = (id) => { const at = model.recent.indexOf(id); return at === -1 ? Infinity : at; };
215
+ const keep = new Set([...kept].sort((a, b) => rank(a.id) - rank(b.id)).slice(0, MAX_REMEMBERED_TABS).map((tab) => tab.id));
216
+ kept = kept.filter((tab) => keep.has(tab.id));
217
+ }
218
+ const active = kept.find((tab) => tab.id === model.activeId) || null;
219
+ return { active: active ? active.path : null, tabs: kept.map(({ id, ...tab }) => tab) };
220
+ }
221
+
222
+ /**
223
+ * A model from what was kept: every file as a tab that is not loaded yet.
224
+ * @param {{ active?: string | null, tabs?: object[] } | null} snapshot from serializeTabs()
225
+ * @returns {{ model: EditorTabs, places: Map<string, { anchor: number, head: number, top: number, dirty: boolean }> }}
226
+ * `places`: by tab id, where each tab was
227
+ */
228
+ export function restoreTabs(snapshot) {
229
+ let model = createTabs();
230
+ const places = new Map();
231
+ let unpinned = false;
232
+ for (const entry of Array.isArray(snapshot?.tabs) ? snapshot.tabs.slice(0, MAX_REMEMBERED_TABS) : []) {
233
+ if (!entry || typeof entry.path !== 'string' || !entry.path) continue;
234
+ const key = tabKey({ path: entry.path });
235
+ if (findTab(model, key)) continue;
236
+ // At most one tab is waiting to be reused; one with unsaved text never is.
237
+ const pinned = entry.pinned !== false || !!entry.dirty || unpinned;
238
+ if (!pinned) unpinned = true;
239
+ const seq = model.seq + 1;
240
+ const tab = { id: `tab-${seq}`, key, kind: 'file', path: normalizePath(entry.path), name: fileName(entry.path), pinned };
241
+ model = { ...model, tabs: [...model.tabs, tab], seq };
242
+ places.set(tab.id, { anchor: position(entry.anchor), head: position(entry.head), top: position(entry.top), dirty: !!entry.dirty });
243
+ }
244
+ const active = (typeof snapshot?.active === 'string' && findTab(model, tabKey({ path: snapshot.active }))) || model.tabs[0] || null;
245
+ if (active) model = { ...model, activeId: active.id, recent: [active.id] };
246
+ return { model, places };
247
+ }
248
+
249
+ const isObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
250
+
251
+ /**
252
+ * The stored value with one project's tabs replaced. A project with no tab open is forgotten; beyond
253
+ * MAX_REMEMBERED_PROJECTS the ones not opened for the longest time are.
254
+ * @param {object | null} stored what storage holds (parsed), or anything
255
+ * @param {string} project the project's folder, '' when none is selected
256
+ * @param {{ active: string | null, tabs: object[] }} snapshot from serializeTabs()
257
+ * @param {number} now
258
+ */
259
+ export function rememberTabs(stored, project, snapshot, now) {
260
+ const projects = { ...(isObject(stored) && stored.v === REMEMBERED_VERSION && isObject(stored.projects) ? stored.projects : {}) };
261
+ const name = String(project ?? '');
262
+ if (snapshot?.tabs?.length) projects[name] = { at: now, active: snapshot.active ?? null, tabs: snapshot.tabs };
263
+ else delete projects[name];
264
+ const names = Object.keys(projects);
265
+ if (names.length > MAX_REMEMBERED_PROJECTS) {
266
+ names.sort((a, b) => (Number(projects[b]?.at) || 0) - (Number(projects[a]?.at) || 0));
267
+ for (const old of names.slice(MAX_REMEMBERED_PROJECTS)) delete projects[old];
268
+ }
269
+ return { v: REMEMBERED_VERSION, projects };
270
+ }
271
+
272
+ /** What is kept for a project, or null. */
273
+ export function recallTabs(stored, project) {
274
+ if (!isObject(stored) || stored.v !== REMEMBERED_VERSION || !isObject(stored.projects)) return null;
275
+ const entry = stored.projects[String(project ?? '')];
276
+ return isObject(entry) && Array.isArray(entry.tabs) && entry.tabs.length ? { active: entry.active ?? null, tabs: entry.tabs } : null;
277
+ }
278
+
279
+ // ── A file that has another path now ──
280
+
281
+ const withoutEndSlash = (path) => { const text = normalizePath(path); return text.length > 1 ? text.replace(/\/+$/, '') : text; };
282
+
283
+ /**
284
+ * Where `path` is after `from` was renamed or moved to `to`: `from` may be the file itself or a folder that holds
285
+ * it.
286
+ * @returns {string | null} null when `path` is neither `from` nor inside it
287
+ */
288
+ export function repointPath(path, from, to) {
289
+ const at = normalizePath(path);
290
+ const old = withoutEndSlash(from);
291
+ const now = withoutEndSlash(to);
292
+ if (!at || !old || !now) return null;
293
+ if (at === old) return now;
294
+ return at.startsWith(`${old}/`) ? now + at.slice(old.length) : null;
295
+ }
296
+
297
+ /**
298
+ * What is kept of the tabs (of every project) after a file, or a folder of files, was renamed or moved: each tab of
299
+ * it is kept at its new path, with its place, its pin and its unsaved mark. A tab that was already kept for the
300
+ * new path gives way to the one that moved there (the file it named was replaced).
301
+ * @param {object | null} stored what storage holds (parsed), or anything
302
+ * @returns {{ stored: object | null, moved: { from: string, to: string, dirty: boolean }[] }} `stored` is the value
303
+ * to write, the same object when nothing changed; `moved`: the kept tabs that have another path now
304
+ */
305
+ export function repointRemembered(stored, from, to) {
306
+ if (!isObject(stored) || stored.v !== REMEMBERED_VERSION || !isObject(stored.projects)) return { stored, moved: [] };
307
+ const moved = new Map(); // the path a tab was kept at → { from, to, dirty }
308
+ const projects = {};
309
+ for (const [name, entry] of Object.entries(stored.projects)) {
310
+ const tabs = isObject(entry) && Array.isArray(entry.tabs) ? entry.tabs : [];
311
+ const next = tabs.map((tab) => {
312
+ const now = tab && typeof tab.path === 'string' ? repointPath(tab.path, from, to) : null;
313
+ return now && now !== normalizePath(tab.path) ? now : null;
314
+ });
315
+ if (!next.some(Boolean)) { projects[name] = entry; continue; }
316
+ // One tab per file: the one that moved stays, a tab that was kept for the path it moved to goes.
317
+ const arrived = new Set(next.filter(Boolean));
318
+ const kept = [];
319
+ tabs.forEach((tab, at) => {
320
+ if (!next[at]) { if (!(tab && arrived.has(normalizePath(tab.path)))) kept.push(tab); return; }
321
+ const old = normalizePath(tab.path);
322
+ const pair = moved.get(old) || { from: old, to: next[at], dirty: false };
323
+ pair.dirty = pair.dirty || !!tab.dirty;
324
+ moved.set(old, pair);
325
+ kept.push({ ...tab, path: next[at] });
326
+ });
327
+ const active = typeof entry.active === 'string' ? repointPath(entry.active, from, to) || entry.active : entry.active ?? null;
328
+ projects[name] = { ...entry, active, tabs: kept };
329
+ }
330
+ return moved.size ? { stored: { ...stored, projects }, moved: [...moved.values()] } : { stored, moved: [] };
331
+ }
@@ -0,0 +1,233 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — Code editor: the tab strip
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // The row of tabs under the editor's header (#fe-editor-tabs, markup in
6
+ // html-shell.js, look in styles.css). It is there only while two or more
7
+ // documents are open: with one, the panel is as it was before it had tabs.
8
+ // It draws what editor-tabs-logic.js holds and reports what the user did;
9
+ // editor-panel.js decides what that means. No imports: the strings, the file
10
+ // icons and the menu are handed in.
11
+
12
+ const FILE_ICON = '<svg viewBox="0 0 24 24"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z"/><polyline points="13 2 13 7 18 7"/></svg>';
13
+ const PLAN_ICON = '<svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><polyline points="9 15 11 17 15 13"/></svg>';
14
+ const CLOSE_ICON = '<svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
15
+
16
+ /**
17
+ * @param {object} o
18
+ * @param {HTMLElement} o.root #fe-editor-tabs
19
+ * @param {(key: string, params?: object) => string} o.t
20
+ * @param {(fileName: string) => ({ svg?: string, img?: string, color?: string | null } | null)} [o.fileIcon] the
21
+ * File Explorer's icon for a file name
22
+ * @param {(menu: object) => object} o.openMenu editor-menu.js, already given the panel as its host
23
+ * @param {object} o.on what the user did, by tab id: `activate`, `close`, `pin`, `move(id, index)`,
24
+ * `keyboardLeft()` (the strip was hidden while the keyboard was in it), and
25
+ * `command(id, 'close' | 'closeOthers' | 'closeSaved' | 'copyPath' | 'reveal')` for the menu
26
+ */
27
+ export function createTabStrip({ root, t, fileIcon = () => null, openMenu, on }) {
28
+ let tabs = [];
29
+ let drawn = ''; // what the strip shows, as text: the same again is not drawn again
30
+ let dragged = null;
31
+
32
+ const tabOf = (target) => target?.closest?.('.fe-editor-tab') || null;
33
+ const nodeOf = (id) => [...root.children].find((node) => node.dataset.id === id) || null;
34
+
35
+ function iconFor(tab) {
36
+ const icon = document.createElement('span');
37
+ icon.className = 'fe-editor-tab-icon';
38
+ icon.setAttribute('aria-hidden', 'true');
39
+ if (tab.kind !== 'file') { icon.innerHTML = PLAN_ICON; return icon; }
40
+ const found = fileIcon(tab.name);
41
+ if (found?.img) {
42
+ const image = document.createElement('img');
43
+ image.src = found.img;
44
+ image.alt = '';
45
+ icon.append(image);
46
+ } else {
47
+ icon.innerHTML = found?.svg || FILE_ICON;
48
+ if (found?.color) icon.style.color = found.color;
49
+ }
50
+ return icon;
51
+ }
52
+
53
+ /**
54
+ * Draw the strip.
55
+ * @param {object} o
56
+ * @param {{ id: string, kind: string, name: string, path: string, pinned: boolean }[]} o.tabs in order
57
+ * @param {string | null} o.activeId
58
+ * @param {Map<string, { name: string, hint: string }>} o.labels from tabLabels()
59
+ * @param {(id: string) => boolean} o.isDirty
60
+ * @param {(id: string) => string} o.titleOf the path a tab shows as its tooltip
61
+ */
62
+ function render({ tabs: next, activeId, labels, isDirty, titleOf }) {
63
+ const rows = next.map((tab) => ({ tab, active: tab.id === activeId, dirty: isDirty(tab.id), label: labels.get(tab.id) || { name: tab.name, hint: '' }, title: titleOf(tab.id) }));
64
+ const signature = JSON.stringify(rows.map((entry) => [entry.tab.id, entry.tab.kind, entry.tab.pinned, entry.active, entry.dirty, entry.label.name, entry.label.hint, entry.title]));
65
+ tabs = next;
66
+ // Unchanged: the elements stay as they are (a second click of a double click finds the one the first hit).
67
+ if (signature === drawn) return;
68
+ drawn = signature;
69
+ const held = root.contains(document.activeElement); // the keyboard is in the strip
70
+ const focused = held ? tabOf(document.activeElement)?.dataset.id : null;
71
+ const scrolled = root.scrollLeft;
72
+ root.hidden = tabs.length < 2;
73
+ root.textContent = '';
74
+ if (root.hidden) {
75
+ // The strip is gone with the keyboard in it (a tab was closed from it and one file is left): the keyboard
76
+ // would fall to the page, where the next key is somebody else's shortcut. The host says where it goes.
77
+ if (held) on.keyboardLeft?.();
78
+ return;
79
+ }
80
+ for (const { tab, active, dirty, label, title } of rows) {
81
+ const node = document.createElement('div');
82
+ node.className = `fe-editor-tab${active ? ' active' : ''}${dirty ? ' dirty' : ''}${tab.pinned ? '' : ' preview'}${tab.kind === 'file' ? '' : ' marked'}`;
83
+ node.id = `fe-editor-tab-${tab.id}`;
84
+ node.dataset.id = tab.id;
85
+ node.setAttribute('role', 'tab');
86
+ node.setAttribute('aria-selected', String(active));
87
+ node.tabIndex = active ? 0 : -1;
88
+ node.draggable = true;
89
+ node.dataset.tooltip = title;
90
+
91
+ const name = document.createElement('span');
92
+ name.className = 'fe-editor-tab-name';
93
+ name.textContent = label.name;
94
+ node.append(iconFor(tab), name);
95
+ if (tab.kind !== 'file') {
96
+ const mark = document.createElement('span');
97
+ mark.className = 'fe-editor-tab-mark';
98
+ mark.textContent = t(tab.kind === 'plan' ? 'editor.tabs.plan' : 'editor.tabs.changelog');
99
+ node.append(mark);
100
+ } else if (label.hint) {
101
+ const hint = document.createElement('span');
102
+ hint.className = 'fe-editor-tab-hint';
103
+ hint.textContent = label.hint;
104
+ node.append(hint);
105
+ }
106
+ const close = document.createElement('button');
107
+ close.type = 'button';
108
+ close.className = 'fe-editor-tab-close';
109
+ close.tabIndex = -1;
110
+ close.setAttribute('aria-label', t(dirty ? 'editor.tabs.closeUnsaved' : 'editor.tabs.close', { name: tab.name }));
111
+ close.innerHTML = `<span class="fe-editor-tab-dot" aria-hidden="true"></span>${CLOSE_ICON}`;
112
+ node.append(close);
113
+ root.append(node);
114
+ }
115
+ root.scrollLeft = scrolled;
116
+ const activeNode = nodeOf(activeId);
117
+ // The shown tab is in sight; the keyboard stays where it was.
118
+ activeNode?.scrollIntoView({ block: 'nearest', inline: 'nearest' });
119
+ if (focused) (nodeOf(focused) || activeNode)?.focus();
120
+ }
121
+
122
+ function menuFor(node, at) {
123
+ const id = node.dataset.id;
124
+ openMenu({
125
+ anchor: at ? null : node,
126
+ at,
127
+ label: t('editor.tabs.menu.label'),
128
+ items: [
129
+ { id: 'close', label: t('editor.tabs.menu.close') },
130
+ { id: 'closeOthers', label: t('editor.tabs.menu.closeOthers'), disabled: tabs.length < 2 },
131
+ { id: 'closeSaved', label: t('editor.tabs.menu.closeSaved') },
132
+ { separator: true },
133
+ { id: 'copyPath', label: t('editor.tabs.menu.copyPath') },
134
+ { id: 'reveal', label: t('editor.tabs.menu.reveal') },
135
+ ],
136
+ onSelect: (action) => on.command(id, action),
137
+ });
138
+ }
139
+
140
+ root.addEventListener('click', (event) => {
141
+ const node = tabOf(event.target);
142
+ if (!node) return;
143
+ if (event.target.closest('.fe-editor-tab-close')) { event.stopPropagation(); on.close(node.dataset.id); return; }
144
+ // The second click of a double click keeps the tab. (Counted here: the first click may have redrawn the
145
+ // strip, and a `dblclick` would then go to an element that is no longer in it.)
146
+ if (event.detail >= 2) on.pin(node.dataset.id);
147
+ else on.activate(node.dataset.id);
148
+ });
149
+ // The middle button closes a tab; held down, it would otherwise start the browser's own scrolling.
150
+ root.addEventListener('mousedown', (event) => { if (event.button === 1 && tabOf(event.target)) event.preventDefault(); });
151
+ root.addEventListener('auxclick', (event) => {
152
+ const node = tabOf(event.target);
153
+ if (node && event.button === 1) { event.preventDefault(); on.close(node.dataset.id); }
154
+ });
155
+ root.addEventListener('contextmenu', (event) => {
156
+ const node = tabOf(event.target);
157
+ if (!node) return;
158
+ event.preventDefault();
159
+ event.stopPropagation();
160
+ menuFor(node, { x: event.clientX, y: event.clientY });
161
+ });
162
+ // A vertical wheel moves along the strip.
163
+ root.addEventListener('wheel', (event) => {
164
+ if (event.deltaX || !event.deltaY || root.scrollWidth <= root.clientWidth) return;
165
+ root.scrollLeft += event.deltaY;
166
+ event.preventDefault();
167
+ }, { passive: false });
168
+
169
+ for (const type of ['keydown', 'keyup', 'keypress']) {
170
+ root.addEventListener(type, (event) => {
171
+ // The strip is outside the editor's own element: its keys are stopped here.
172
+ event.stopPropagation();
173
+ if (type !== 'keydown') return;
174
+ const node = tabOf(event.target);
175
+ if (!node) return;
176
+ const at = tabs.findIndex((tab) => tab.id === node.dataset.id);
177
+ const go = (index) => {
178
+ const target = tabs[(index + tabs.length) % tabs.length];
179
+ if (!target) return;
180
+ event.preventDefault();
181
+ on.activate(target.id, { keepFocus: true });
182
+ nodeOf(target.id)?.focus();
183
+ };
184
+ if (event.altKey && !event.ctrlKey && !event.metaKey) {
185
+ // The editor's own tab keys work from here too.
186
+ if (event.code === 'Period') go(at + 1);
187
+ else if (event.code === 'Comma') go(at - 1);
188
+ else if (event.code === 'KeyW') { event.preventDefault(); if (!event.repeat) on.close(node.dataset.id); }
189
+ return;
190
+ }
191
+ switch (event.key) {
192
+ case 'ArrowRight': go(at + 1); break;
193
+ case 'ArrowLeft': go(at - 1); break;
194
+ case 'Home': go(0); break;
195
+ case 'End': go(tabs.length - 1); break;
196
+ case 'Enter': case ' ': event.preventDefault(); on.activate(node.dataset.id); break;
197
+ // A key that is held closes one tab, not one after the other.
198
+ case 'Delete': case 'Backspace': event.preventDefault(); if (!event.repeat) on.close(node.dataset.id); break;
199
+ case 'ContextMenu': event.preventDefault(); menuFor(node, null); break;
200
+ case 'F10': if (event.shiftKey) { event.preventDefault(); menuFor(node, null); } break;
201
+ default:
202
+ }
203
+ });
204
+ }
205
+
206
+ // Dragging a tab onto another puts it there.
207
+ root.addEventListener('dragstart', (event) => {
208
+ const node = tabOf(event.target);
209
+ if (!node) return;
210
+ dragged = node.dataset.id;
211
+ event.dataTransfer.effectAllowed = 'move';
212
+ event.dataTransfer.setData('text/plain', node.dataset.id);
213
+ node.classList.add('dragging');
214
+ });
215
+ root.addEventListener('dragover', (event) => {
216
+ if (!dragged || !tabOf(event.target)) return;
217
+ event.preventDefault();
218
+ event.dataTransfer.dropEffect = 'move';
219
+ });
220
+ root.addEventListener('drop', (event) => {
221
+ const node = tabOf(event.target);
222
+ if (!dragged || !node) return;
223
+ event.preventDefault();
224
+ const to = tabs.findIndex((tab) => tab.id === node.dataset.id);
225
+ if (to >= 0 && node.dataset.id !== dragged) on.move(dragged, to);
226
+ });
227
+ root.addEventListener('dragend', () => {
228
+ nodeOf(dragged)?.classList.remove('dragging');
229
+ dragged = null;
230
+ });
231
+
232
+ return { render, element: root };
233
+ }