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,790 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — File Explorer: the tree
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // Draws the rows of #fe-tree and takes what the user does to them: pointer,
6
+ // keyboard, drag and drop, a name typed in place. It holds no file data: it
7
+ // asks the controller (fx-main.js) what a folder shows and what a row looks
8
+ // like, and reports what was done. What a key or a click means is decided in
9
+ // fx-logic.js.
10
+ //
11
+ // The rows are an ARIA tree: `.fe-node` (treeitem) > `.fe-row` + `.fe-children`
12
+ // (group). The tree element itself has the keyboard; the row it is on is its
13
+ // `aria-activedescendant`. A folder is drawn in chunks, and read again without
14
+ // being rebuilt: rows are matched by name, so what is open, selected, scrolled
15
+ // to or being renamed stays where it is.
16
+ //
17
+ // A line among the rows that offers something ("Show more", "Retry") is a stop
18
+ // of the keyboard as a row is: the arrows reach it in its place, Enter or
19
+ // Space does what it offers, and the keyboard then goes to the first row that
20
+ // came, or back to the folder. It is never selected: it is not a file.
21
+
22
+ import { canDropInto, dirName, joinPath, keyAction, moveTarget, pickRow, rangeBetween, topLevelOf, typeAheadTarget } from './fx-logic.js';
23
+
24
+ const CHUNK = 300;
25
+ const TYPE_AHEAD_MS = 700;
26
+ const LONG_PRESS_MS = 500;
27
+ const SPRING_OPEN_MS = 700;
28
+ const CHEVRON = '<svg viewBox="0 0 24 24" aria-hidden="true"><polyline points="9 6 15 12 9 18"/></svg>';
29
+ const LINK_GLYPH = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 17 17 7"/><polyline points="8 7 17 7 17 16"/></svg>';
30
+ let _seq = 0;
31
+
32
+ /**
33
+ * @param {object} o
34
+ * @param {HTMLElement} o.el #fe-tree
35
+ * @param {boolean} o.mac
36
+ * @param {{ folder: string, link: string, forFile: (name: string) => { svg?: string, img?: string, color?: string | null } }} o.icons
37
+ * @param {object} o.hooks the controller: `itemsOf(dir)`, `isExpanded(path)`, `rowInfo(path, item)`, `words` (the
38
+ * strings a row needs), and what the user did: `onToggle`, `onOpen`, `onMenu`, `onAction`, `onSelection`,
39
+ * `onDrop`, `onDragStart`, `onDragEnd`, `onRetry`, `onScroll`, `onEditLost`
40
+ */
41
+ export function createTree({ el, mac, icons, hooks }) {
42
+ const nodes = new Map(); // path → .fe-node
43
+ const stops = new Map(); // key → a line that offers something (.fe-note with a button), where the keyboard stops
44
+ const shown = new Map(); // folder → how many of its rows are drawn
45
+ let root = null;
46
+ let selected = new Set();
47
+ let anchor = null;
48
+ let focus = null;
49
+ let editing = null; // { node, input, temp, done }
50
+ let dragging = null; // the rows being dragged out of this tree
51
+ let dropInto = null;
52
+ let springTimer = 0; let springPath = null;
53
+ let typed = ''; let typedTimer = 0;
54
+ let wantFirst = false; // the keyboard was on a line of the project's own folder: it takes the first row that comes
55
+ let pressTimer = 0; let swallowClick = false; let pressedAt = 0;
56
+ const coarse = typeof matchMedia === 'function' && matchMedia('(pointer: coarse)').matches;
57
+ const more = typeof IntersectionObserver === 'function'
58
+ ? new IntersectionObserver((entries) => { for (const entry of entries) if (entry.isIntersecting) showMore(entry.target.dataset.dir); }, { root: el })
59
+ : null;
60
+
61
+ el.setAttribute('role', 'tree');
62
+ el.setAttribute('aria-multiselectable', 'true');
63
+ el.tabIndex = 0;
64
+
65
+ // ── Rows ──
66
+
67
+ const containerOf = (path) => (path === root ? el : nodes.get(path)?.querySelector(':scope > .fe-children') || null);
68
+ const rowOf = (path) => nodes.get(path)?.querySelector(':scope > .fe-row') || null;
69
+ const realChildren = (container) => [...container.children].filter((child) => child.classList.contains('fe-node') && !child.dataset.temp);
70
+ // A stop's key cannot be a path: it begins with a character no path holds.
71
+ const stopKey = (act, dir) => `\u0000${act}:${dir}`;
72
+ const isStop = (key) => typeof key === 'string' && key.charCodeAt(0) === 0;
73
+ const there = (key) => nodes.has(key) || stops.has(key);
74
+ const dropNote = (line) => { more?.unobserve(line); if (stops.get(line.dataset.stop) === line) stops.delete(line.dataset.stop); line.remove(); };
75
+
76
+ function forget(node) {
77
+ for (const inner of [node, ...node.querySelectorAll('.fe-node')]) {
78
+ const path = inner.dataset.path;
79
+ if (nodes.get(path) === inner) nodes.delete(path);
80
+ shown.delete(path);
81
+ if (editing?.node === inner) loseEdit();
82
+ }
83
+ }
84
+ function removeNode(node) { forget(node); node.remove(); }
85
+
86
+ function createNode(item, parent, depth) {
87
+ const path = joinPath(parent, item.name);
88
+ const node = document.createElement('div');
89
+ node.className = 'fe-node';
90
+ node.id = `fe-n-${++_seq}`;
91
+ node.setAttribute('role', 'treeitem');
92
+ node.dataset.path = path;
93
+ node.dataset.name = item.name;
94
+ node.dataset.type = item.type;
95
+ node.dataset.depth = String(depth);
96
+ node.dataset.expanded = 'false';
97
+ node.setAttribute('aria-level', String(depth + 1));
98
+ const row = document.createElement('div');
99
+ row.className = 'fe-row';
100
+ row.style.paddingLeft = `${depth * 16 + 8}px`;
101
+ if (!coarse) row.draggable = true;
102
+ const chevron = document.createElement('span');
103
+ chevron.className = item.type === 'dir' ? 'fe-chevron' : 'fe-chevron fe-chevron--none';
104
+ if (item.type === 'dir') chevron.innerHTML = CHEVRON;
105
+ const icon = document.createElement('span');
106
+ icon.className = `fe-icon fe-icon--${item.type === 'dir' ? 'dir' : 'file'}`;
107
+ const name = document.createElement('span');
108
+ name.className = 'fe-name';
109
+ const dot = document.createElement('span');
110
+ dot.className = 'fe-open-dot';
111
+ dot.setAttribute('aria-hidden', 'true');
112
+ const badge = document.createElement('span');
113
+ badge.className = 'fe-git-badge';
114
+ badge.setAttribute('aria-hidden', 'true');
115
+ row.append(chevron, icon, name, dot, badge);
116
+ node.append(row);
117
+ if (item.type === 'dir') {
118
+ const children = document.createElement('div');
119
+ children.className = 'fe-children';
120
+ children.setAttribute('role', 'group');
121
+ children.style.setProperty('--fe-guide', `${depth * 16 + 14}px`);
122
+ node.append(children);
123
+ }
124
+ nodes.set(path, node);
125
+ return node;
126
+ }
127
+
128
+ function drawIcon(icon, item, info) {
129
+ const key = `${item.type}|${item.name}|${info.color || ''}|${info.link ? 1 : 0}`;
130
+ if (icon.dataset.drawn === key) return;
131
+ icon.dataset.drawn = key;
132
+ icon.style.color = '';
133
+ if (item.type === 'dir') {
134
+ icon.innerHTML = icons.folder;
135
+ const svg = icon.querySelector('svg');
136
+ if (svg && info.color) { svg.style.fill = info.color; svg.style.stroke = info.color; }
137
+ } else {
138
+ const found = icons.forFile(item.name);
139
+ if (found?.img) {
140
+ icon.textContent = '';
141
+ const image = document.createElement('img');
142
+ image.src = found.img; image.alt = ''; image.className = 'fe-icon-img';
143
+ icon.append(image);
144
+ } else {
145
+ icon.innerHTML = found?.svg || '';
146
+ if (found?.color) icon.style.color = found.color;
147
+ }
148
+ }
149
+ if (info.link) icon.insertAdjacentHTML('beforeend', `<span class="fe-link-glyph">${LINK_GLYPH}</span>`);
150
+ }
151
+
152
+ // Everything a row shows of its state. Cheap: called for every drawn row when git or the editor changes.
153
+ function updateNode(node, item, index = null, count = null) {
154
+ node._item = item;
155
+ const path = node.dataset.path;
156
+ const info = hooks.rowInfo(path, item);
157
+ const row = node.firstElementChild;
158
+ const [, icon, name, dot, badge] = row.children;
159
+ if (index !== null) { node.setAttribute('aria-posinset', String(index + 1)); node.setAttribute('aria-setsize', String(count)); }
160
+ node.setAttribute('aria-selected', String(selected.has(path)));
161
+ node.classList.toggle('fe-node--selected', selected.has(path));
162
+ node.classList.toggle('fe-node--focus', focus === path);
163
+ node.classList.toggle('fe-node--ignored', !!info.ignored);
164
+ node.classList.toggle('fe-node--cut', !!info.cut);
165
+ node.classList.toggle('fe-node--open', !!info.open);
166
+ node.classList.toggle('fe-node--active', !!info.active);
167
+ node.classList.toggle('fe-node--missing', !!info.missing);
168
+ node.classList.toggle('fe-node--faint', !!info.faint);
169
+ drawIcon(icon, item, info);
170
+ if (editing?.node !== node) {
171
+ const sign = `${item.name}|${info.match ? info.match.join(',') : ''}`;
172
+ if (name.dataset.drawn !== sign) {
173
+ name.dataset.drawn = sign;
174
+ name.textContent = '';
175
+ if (info.match) {
176
+ const [from, to] = info.match;
177
+ const hit = document.createElement('mark');
178
+ hit.className = 'fe-match';
179
+ hit.textContent = item.name.slice(from, to);
180
+ name.append(item.name.slice(0, from), hit, item.name.slice(to));
181
+ } else name.textContent = item.name;
182
+ }
183
+ }
184
+ dot.className = `fe-open-dot${info.dirty ? ' fe-open-dot--dirty' : info.open ? ' fe-open-dot--open' : ''}`;
185
+ badge.className = `fe-git-badge${info.tone ? ` fe-git-${info.tone}` : ''}${info.letter ? '' : ' fe-git-badge--dot'}`;
186
+ badge.textContent = info.letter || '';
187
+ badge.hidden = !info.tone;
188
+ // The whole name, and the row's state in words: for a pointer that rests on it, and for a screen reader.
189
+ const said = info.words?.length ? `${item.name} (${info.words.join(', ')})` : item.name;
190
+ if (row.title !== said) { row.title = said; node.setAttribute('aria-label', said); }
191
+ }
192
+
193
+ function note(container, className, text, action = null) {
194
+ const line = document.createElement('div');
195
+ line.className = `fe-note ${className}`;
196
+ line.style.paddingLeft = `${(Number(container.parentElement?.dataset.depth ?? -1) + 1) * 16 + 26}px`;
197
+ const words = document.createElement('span');
198
+ words.textContent = text;
199
+ line.append(words);
200
+ if (action) {
201
+ const button = document.createElement('button');
202
+ button.type = 'button';
203
+ button.className = 'fe-note-action';
204
+ button.textContent = action.label;
205
+ button.dataset.act = action.act;
206
+ button.tabIndex = -1;
207
+ line.append(button);
208
+ // Where the keyboard stops, like a row: the tree has the keyboard, this line is its active descendant.
209
+ const key = stopKey(action.act, action.dir);
210
+ line.id = `fe-n-${++_seq}`;
211
+ line.dataset.dir = action.dir;
212
+ line.dataset.stop = key;
213
+ line.setAttribute('role', 'treeitem');
214
+ line.setAttribute('aria-label', `${text} ${action.label}`);
215
+ line.classList.toggle('fe-note--focus', focus === key);
216
+ stops.set(key, line);
217
+ }
218
+ container.append(line);
219
+ return line;
220
+ }
221
+
222
+ /** Bring the rows of one folder up to what the controller says it shows now. Rows that stay are not rebuilt. */
223
+ function syncContainer(container, dir, depth) {
224
+ for (const old of [...container.children]) if (old.classList.contains('fe-note')) dropNote(old);
225
+ const data = hooks.itemsOf(dir);
226
+ if (data.state !== 'ready') {
227
+ // What was drawn stays under a folder that is being read again; a first read, or a failure, says so.
228
+ if (data.state === 'error') {
229
+ for (const child of realChildren(container)) removeNode(child);
230
+ note(container, 'fe-note--error', data.message, { label: hooks.words.retry, act: 'retry', dir });
231
+ } else if (!realChildren(container).length) note(container, 'fe-note--loading', hooks.words.loading);
232
+ return;
233
+ }
234
+ const limit = Math.max(CHUNK, shown.get(dir) || CHUNK);
235
+ const visible = data.items.length > limit ? data.items.slice(0, limit) : data.items;
236
+ // Rows are matched by name. What is gone leaves first; what stays is then put in order, moving as few as it takes.
237
+ const existing = new Map(realChildren(container).map((child) => [child.dataset.name, child]));
238
+ const wanted = visible.map((item) => {
239
+ const node = existing.get(item.name);
240
+ if (node && node.dataset.type === item.type) { existing.delete(item.name); return node; }
241
+ return null;
242
+ });
243
+ for (const node of existing.values()) removeNode(node);
244
+ const real = (node) => { let at = node; while (at && (!at.classList.contains('fe-node') || at.dataset.temp)) at = at.nextElementSibling; return at; };
245
+ let cursor = real(container.firstElementChild);
246
+ visible.forEach((item, index) => {
247
+ const node = wanted[index] || createNode(item, dir, depth);
248
+ if (cursor === node) cursor = real(cursor.nextElementSibling);
249
+ else container.insertBefore(node, cursor);
250
+ updateNode(node, item, index, visible.length);
251
+ if (item.type === 'dir') syncDir(node);
252
+ });
253
+ if (data.items.length > visible.length) {
254
+ const rest = note(container, 'fe-note--more', hooks.words.more(data.items.length - visible.length), { label: hooks.words.showMore, act: 'more', dir });
255
+ more?.observe(rest);
256
+ } else if (data.truncated) note(container, 'fe-note--cut', hooks.words.truncated(data.items.length, data.total));
257
+ if (!data.items.length && !container.querySelector(':scope > .fe-node')) note(container, 'fe-note--empty', data.note || hooks.words.emptyFolder);
258
+ }
259
+
260
+ // A folder's own node: open or closed, and its rows when it is open.
261
+ function syncDir(node) {
262
+ const path = node.dataset.path;
263
+ const open = hooks.isExpanded(path);
264
+ node.dataset.expanded = String(open);
265
+ node.setAttribute('aria-expanded', String(open));
266
+ const children = node.querySelector(':scope > .fe-children');
267
+ if (!children) return;
268
+ if (open) syncContainer(children, path, Number(node.dataset.depth) + 1);
269
+ else if (children.firstChild) { for (const child of [...children.children]) { if (child.classList.contains('fe-node')) removeNode(child); else dropNote(child); } }
270
+ }
271
+
272
+ function showMore(dir) {
273
+ if (!dir) return;
274
+ // The keyboard was on "Show more": it goes to the first of the rows that come.
275
+ const line = stops.get(stopKey('more', dir));
276
+ const last = focus === line?.dataset.stop ? line.previousElementSibling : null;
277
+ shown.set(dir, (shown.get(dir) || CHUNK) + CHUNK);
278
+ renderDir(dir);
279
+ const next = last?.nextElementSibling;
280
+ if (last && next?.classList.contains('fe-node')) apply({ selected, anchor, focus: next.dataset.path });
281
+ }
282
+
283
+ // What a line offers, asked for with the keyboard.
284
+ function useStop(key) {
285
+ const line = stops.get(key);
286
+ if (!line) return;
287
+ const dir = line.dataset.dir;
288
+ if (line.querySelector('.fe-note-action')?.dataset.act === 'more') { showMore(dir); return; }
289
+ // Retry: the folder is read again and this line goes. The keyboard waits on the folder, one step above where
290
+ // the rows will be, or the line that says it failed again.
291
+ if (nodes.has(dir)) apply({ selected, anchor, focus: dir });
292
+ hooks.onRetry(dir);
293
+ }
294
+
295
+ /** Draw everything from the project's folder down. */
296
+ function render() {
297
+ if (!root) return;
298
+ syncContainer(el, root, 0);
299
+ settle();
300
+ }
301
+ /** Draw one folder again (the root's path draws all). Nothing happens for a folder that is not shown. */
302
+ function renderDir(path) {
303
+ if (!root) return;
304
+ if (path === root) { render(); return; }
305
+ const node = nodes.get(path);
306
+ if (node) { syncDir(node); settle(); }
307
+ }
308
+ /** Every drawn row takes its state again (git, the editor, what was cut). */
309
+ function refreshRows() { for (const node of nodes.values()) if (node._item) updateNode(node, node._item); }
310
+
311
+ // After rows came or went: the selection and the keyboard only ever point at rows that are there.
312
+ function settle() {
313
+ let changed = false;
314
+ for (const path of [...selected]) if (!nodes.has(path)) { selected.delete(path); changed = true; }
315
+ if (anchor && !nodes.has(anchor)) anchor = null;
316
+ if (focus && !there(focus)) {
317
+ // The row the keyboard was on is gone: the folder that held it takes it. A line that offered something (it
318
+ // begins with its folder) is gone when it was used: its folder, likewise. A line of the project's own
319
+ // folder has no row above it: the keyboard takes the first row that comes (below).
320
+ const line = isStop(focus);
321
+ let at = line ? focus.slice(focus.indexOf(':') + 1) : dirName(focus);
322
+ while (at && at !== root && !nodes.has(at)) at = dirName(at);
323
+ focus = nodes.has(at) ? at : null;
324
+ if (!focus && line) wantFirst = true;
325
+ changed = true;
326
+ }
327
+ if (wantFirst && !focus) {
328
+ // The first of the rows that came, or the line that says the folder could not be read again. It has the
329
+ // keyboard and is not selected: nobody picked it.
330
+ const first = order({ withStops: true })[0];
331
+ if (first) { wantFirst = false; apply({ selected, anchor, focus: first }, { scroll: false, quiet: true }); changed = true; }
332
+ }
333
+ markFocus();
334
+ if (changed) hooks.onSelection(selection());
335
+ }
336
+
337
+ // ── Selection and the keyboard's row ──
338
+
339
+ /** The rows in the order they are shown. `withStops`: and the lines the keyboard stops on, each in its place. */
340
+ function order({ withStops = false } = {}) {
341
+ const out = [];
342
+ const walk = (container) => {
343
+ for (const child of container.children) {
344
+ if (withStops && child.dataset.stop) { out.push(child.dataset.stop); continue; }
345
+ if (!child.classList.contains('fe-node') || child.dataset.temp) continue;
346
+ out.push(child.dataset.path);
347
+ if (child.dataset.expanded === 'true') { const inner = child.querySelector(':scope > .fe-children'); if (inner) walk(inner); }
348
+ }
349
+ };
350
+ walk(el);
351
+ return out;
352
+ }
353
+ // The row the keyboard is on, for whoever acts on files: a line that offers something is not one.
354
+ const selection = () => ({ selected: [...selected], anchor, focus: isStop(focus) ? null : focus });
355
+
356
+ function markFocus() {
357
+ const node = focus ? nodes.get(focus) || stops.get(focus) : null;
358
+ if (node) el.setAttribute('aria-activedescendant', node.id); else el.removeAttribute('aria-activedescendant');
359
+ }
360
+ function apply(next, { scroll = true, quiet = false } = {}) {
361
+ const touched = new Set([...selected, ...next.selected, focus, next.focus]);
362
+ // Only rows are selected: a line that offers something can have the keyboard, and nothing else.
363
+ selected = new Set([...next.selected].filter((path) => !isStop(path)));
364
+ anchor = isStop(next.anchor) ? null : next.anchor;
365
+ focus = next.focus;
366
+ if (focus) wantFirst = false;
367
+ for (const path of touched) {
368
+ if (isStop(path)) { stops.get(path)?.classList.toggle('fe-note--focus', focus === path); continue; }
369
+ const node = path ? nodes.get(path) : null;
370
+ if (!node) continue;
371
+ node.classList.toggle('fe-node--selected', selected.has(path));
372
+ node.classList.toggle('fe-node--focus', focus === path);
373
+ node.setAttribute('aria-selected', String(selected.has(path)));
374
+ }
375
+ markFocus();
376
+ if (scroll && focus) (isStop(focus) ? stops.get(focus) : rowOf(focus))?.scrollIntoView({ block: 'nearest' });
377
+ if (!quiet) hooks.onSelection(selection());
378
+ }
379
+ /** Select rows (those that are drawn), and put the keyboard on one of them. */
380
+ function select(paths, { focus: at = null, scroll = true, quiet = false } = {}) {
381
+ const list = paths.filter((path) => nodes.has(path));
382
+ const on = at && nodes.has(at) ? at : list[0] || null;
383
+ apply({ selected: new Set(list), anchor: on, focus: on }, { scroll, quiet });
384
+ }
385
+
386
+ // ── A name typed in place (rename, new file, new folder) ──
387
+
388
+ // The row a name was being typed in is gone. That is a loss to say when the file went away under the typing;
389
+ // it is what was asked for when the name was just kept and the row gives way to the one of the new name. While
390
+ // the name is on its way to the server nobody knows which: a refresh that follows the disk may take the row
391
+ // before the answer is here. Then nothing is said yet, and the answer decides (submit).
392
+ function loseEdit() {
393
+ const lost = editing;
394
+ editing = null;
395
+ if (!lost) return;
396
+ lost.tip?.remove();
397
+ if (lost.busy) { lost.lost = true; return; }
398
+ if (!lost.settled) hooks.onEditLost?.(lost.original);
399
+ }
400
+ /** The name that was typed is the entry's name now: the row it was typed in is about to be replaced. */
401
+ function settleEdit() { if (editing) editing.settled = true; }
402
+
403
+ /**
404
+ * Let the user type a name in a row.
405
+ * @param {object} o
406
+ * @param {string} [o.path] the row to rename; or
407
+ * @param {string} [o.parent] the folder a new row is added to, and
408
+ * @param {'file' | 'dir'} [o.kind] what the new row will be
409
+ * @param {string} [o.value] @param {[number, number]} [o.select] the part of it that starts selected
410
+ * @param {string} o.label the field's accessible name
411
+ * @param {(name: string) => string | null} o.validate the sentence that says why a name cannot be used
412
+ * @param {(name: string) => Promise<{ ok: boolean, message?: string }>} o.commit
413
+ * @returns {boolean} whether the field is there
414
+ */
415
+ function startEdit({ path = null, parent = null, kind = 'file', value = '', select: range = null, label, validate, commit }) {
416
+ endEdit();
417
+ let node; let temp = false;
418
+ if (path) {
419
+ node = nodes.get(path);
420
+ if (!node) return false;
421
+ } else {
422
+ const container = containerOf(parent);
423
+ if (!container) return false;
424
+ const depth = parent === root ? 0 : Number(nodes.get(parent).dataset.depth) + 1;
425
+ node = createNode({ name: '', type: kind }, parent, depth);
426
+ nodes.delete(node.dataset.path);
427
+ node.dataset.temp = '1';
428
+ node.removeAttribute('role');
429
+ drawIcon(node.firstElementChild.children[1], { name: kind === 'dir' ? '' : 'new', type: kind }, {});
430
+ node.querySelector('.fe-git-badge').hidden = true;
431
+ container.querySelector(':scope > .fe-note--empty')?.remove();
432
+ container.insertBefore(node, container.firstChild);
433
+ temp = true;
434
+ }
435
+ const row = node.firstElementChild;
436
+ const name = row.children[2];
437
+ const input = document.createElement('input');
438
+ input.type = 'text';
439
+ input.className = 'fe-rename-input';
440
+ input.value = value;
441
+ input.spellcheck = false;
442
+ input.autocomplete = 'off';
443
+ input.setAttribute('autocapitalize', 'off');
444
+ input.setAttribute('aria-label', label);
445
+ const tip = document.createElement('div');
446
+ tip.className = 'fe-rename-tip';
447
+ tip.id = `${node.id}-tip`;
448
+ tip.setAttribute('role', 'alert');
449
+ tip.hidden = true;
450
+ input.setAttribute('aria-describedby', tip.id);
451
+ name.textContent = '';
452
+ name.dataset.drawn = '';
453
+ name.append(input);
454
+ row.after(tip);
455
+ row.draggable = false;
456
+ const state = { node, input, tip, temp, original: value, busy: false, lost: false };
457
+ editing = state;
458
+ const say = (message) => { tip.textContent = message || ''; tip.hidden = !message; input.setAttribute('aria-invalid', String(!!message)); };
459
+
460
+ function finish() {
461
+ if (editing !== state) return;
462
+ editing = null;
463
+ tip.remove();
464
+ if (temp) node.remove();
465
+ else {
466
+ if (!coarse) row.draggable = true;
467
+ if (node.isConnected && node._item) updateNode(node, node._item);
468
+ }
469
+ }
470
+ async function submit() {
471
+ if (state.busy || editing !== state) return;
472
+ const typedName = input.value.trim();
473
+ // Nothing typed, or the name it had: nothing to do.
474
+ if (!typedName || (!temp && typedName === value)) { finish(); el.focus({ preventScroll: true }); return; }
475
+ const problem = validate(typedName);
476
+ if (problem) { say(problem); input.focus(); return; }
477
+ state.busy = true;
478
+ input.readOnly = true;
479
+ let done;
480
+ try { done = await commit(typedName); } catch { done = { ok: false, message: '' }; }
481
+ state.busy = false;
482
+ if (editing !== state) {
483
+ // The row went while the name was on its way. A name that was kept is no loss; one that was not is said,
484
+ // in the server's words when it had any.
485
+ if (state.lost && !done?.ok && !state.settled) hooks.onEditLost?.(state.original, done?.message || '');
486
+ return;
487
+ }
488
+ if (done?.ok) { finish(); return; }
489
+ input.readOnly = false;
490
+ if (done?.cancel) { finish(); el.focus({ preventScroll: true }); return; }
491
+ say(done?.message || '');
492
+ input.focus();
493
+ }
494
+ state.done = finish;
495
+ input.addEventListener('input', () => { const text = input.value.trim(); say(text ? validate(text) : ''); });
496
+ for (const type of ['keydown', 'keyup', 'keypress']) {
497
+ input.addEventListener(type, (event) => {
498
+ event.stopPropagation();
499
+ if (type !== 'keydown') return;
500
+ if (event.key === 'Enter') { event.preventDefault(); submit(); }
501
+ else if (event.key === 'Escape') { event.preventDefault(); finish(); el.focus({ preventScroll: true }); }
502
+ });
503
+ }
504
+ // Leaving the field keeps what was typed when it is a name that can be used, and gives it up when not.
505
+ input.addEventListener('blur', () => {
506
+ if (state.busy || editing !== state) return;
507
+ const text = input.value.trim();
508
+ if (text && !(!temp && text === value) && !validate(text)) submit(); else finish();
509
+ });
510
+ for (const type of ['click', 'dblclick', 'mousedown', 'pointerdown', 'contextmenu']) input.addEventListener(type, (event) => event.stopPropagation());
511
+ input.focus({ preventScroll: true });
512
+ if (range) input.setSelectionRange(range[0], range[1]); else input.select();
513
+ row.scrollIntoView({ block: 'nearest' });
514
+ return true;
515
+ }
516
+ function endEdit() { editing?.done?.(); }
517
+
518
+ // ── Pointer ──
519
+
520
+ const nodeAt = (target) => { const row = target?.closest?.('.fe-row'); const node = row?.parentElement; return node && nodes.get(node.dataset.path) === node ? node : null; };
521
+
522
+ el.addEventListener('click', (event) => {
523
+ if (swallowClick) { swallowClick = false; event.preventDefault(); return; }
524
+ const button = event.target.closest('.fe-note-action');
525
+ if (button) {
526
+ const dir = button.closest('.fe-note').dataset.dir;
527
+ if (button.dataset.act === 'more') showMore(dir); else hooks.onRetry(dir);
528
+ return;
529
+ }
530
+ if (event.target.closest('.fe-note')) return;
531
+ const node = nodeAt(event.target);
532
+ if (!node || editing?.node === node) return;
533
+ const path = node.dataset.path;
534
+ const toggle = mac ? event.metaKey : event.ctrlKey;
535
+ const range = event.shiftKey;
536
+ apply(pickRow({ selected, anchor }, path, { toggle, range }, order()), { scroll: false });
537
+ if (toggle || range || event.detail > 1) return;
538
+ // A click opens or closes a folder; on a file it is the row's menu, the same one a right click opens
539
+ // (docs/design/code-editor/SPEC.md, run 3). A double click opens the file.
540
+ if (node.dataset.type === 'dir') hooks.onToggle(path, node.dataset.expanded !== 'true');
541
+ else menuFor(node, { y: event.clientY });
542
+ });
543
+
544
+ el.addEventListener('dblclick', (event) => {
545
+ const node = nodeAt(event.target);
546
+ if (!node || node.dataset.type === 'dir' || editing?.node === node) return;
547
+ event.preventDefault();
548
+ hooks.onOpen(node.dataset.path, node._item, { pin: true });
549
+ });
550
+
551
+ function menuFor(node, { keyboard = false, y = null } = {}) {
552
+ if (node) {
553
+ const path = node.dataset.path;
554
+ if (!selected.has(path)) apply({ selected: new Set([path]), anchor: path, focus: path }, { scroll: false });
555
+ else if (focus !== path) apply({ selected, anchor, focus: path }, { scroll: false });
556
+ hooks.onMenu({ paths: topLevelOf([...selected]), row: node.firstElementChild, item: node._item, expanded: node.dataset.expanded === 'true', keyboard });
557
+ } else hooks.onMenu({ paths: [], row: null, y, keyboard });
558
+ }
559
+
560
+ el.addEventListener('contextmenu', (event) => {
561
+ if (event.target.closest('.fe-rename-input')) return;
562
+ event.preventDefault();
563
+ event.stopPropagation();
564
+ // A long press already opened it (some touch browsers send both).
565
+ if (Date.now() - pressedAt < 800) return;
566
+ menuFor(nodeAt(event.target), { y: event.clientY });
567
+ });
568
+
569
+ // A finger held on a row opens its menu; one that moves is scrolling.
570
+ el.addEventListener('pointerdown', (event) => {
571
+ clearTimeout(pressTimer);
572
+ if (event.pointerType === 'mouse' || editing) return;
573
+ const node = nodeAt(event.target);
574
+ const from = { x: event.clientX, y: event.clientY };
575
+ const stop = () => { clearTimeout(pressTimer); el.removeEventListener('pointermove', moved); el.removeEventListener('pointerup', stop); el.removeEventListener('pointercancel', stop); };
576
+ const moved = (move) => { if (Math.hypot(move.clientX - from.x, move.clientY - from.y) > 10) stop(); };
577
+ el.addEventListener('pointermove', moved);
578
+ el.addEventListener('pointerup', stop);
579
+ el.addEventListener('pointercancel', stop);
580
+ pressTimer = setTimeout(() => {
581
+ stop();
582
+ pressedAt = Date.now();
583
+ swallowClick = true;
584
+ setTimeout(() => { swallowClick = false; }, 700);
585
+ try { navigator.vibrate?.(8); } catch { /* not there */ }
586
+ menuFor(node, { y: from.y });
587
+ }, LONG_PRESS_MS);
588
+ });
589
+
590
+ // ── Drag and drop ──
591
+
592
+ function markDrop(into) {
593
+ if (dropInto === into) return;
594
+ if (dropInto) (dropInto === root ? el : nodes.get(dropInto))?.classList.remove('fe-drop');
595
+ dropInto = into;
596
+ if (dropInto) (dropInto === root ? el : nodes.get(dropInto))?.classList.add('fe-drop');
597
+ }
598
+ function spring(path) {
599
+ if (springPath === path) return;
600
+ clearTimeout(springTimer);
601
+ springPath = path;
602
+ // Held over a closed folder, the folder opens.
603
+ if (path && nodes.get(path)?.dataset.expanded !== 'true') springTimer = setTimeout(() => { if (springPath === path) hooks.onToggle(path, true); }, SPRING_OPEN_MS);
604
+ }
605
+ // The folder a drop at this point goes into: the folder under the pointer, a file's folder, or the project.
606
+ function folderAt(target) {
607
+ const node = nodeAt(target) || target?.closest?.('.fe-node');
608
+ if (!node || !nodes.has(node.dataset.path)) return root;
609
+ return node.dataset.type === 'dir' ? node.dataset.path : dirName(node.dataset.path);
610
+ }
611
+
612
+ el.addEventListener('dragstart', (event) => {
613
+ const node = nodeAt(event.target);
614
+ if (!node || editing) { event.preventDefault(); return; }
615
+ const path = node.dataset.path;
616
+ if (!selected.has(path)) apply({ selected: new Set([path]), anchor: path, focus: path }, { scroll: false });
617
+ dragging = topLevelOf([...selected]);
618
+ event.dataTransfer.effectAllowed = 'copyMove';
619
+ event.dataTransfer.setData('text/plain', dragging.join('\n'));
620
+ event.dataTransfer.setData('application/x-synabun-paths', JSON.stringify(dragging));
621
+ el.classList.add('fe-tree--dragging');
622
+ hooks.onDragStart(dragging);
623
+ });
624
+ el.addEventListener('dragover', (event) => {
625
+ const files = !dragging && [...(event.dataTransfer?.types || [])].includes('Files');
626
+ if (!dragging && !files) return;
627
+ const into = folderAt(event.target);
628
+ const copy = files || (mac ? event.altKey : event.ctrlKey);
629
+ if (!into || (dragging && !canDropInto(dragging, into, { copy })) || (files && !hooks.acceptsFiles())) { markDrop(null); spring(null); return; }
630
+ event.preventDefault();
631
+ event.dataTransfer.dropEffect = copy ? 'copy' : 'move';
632
+ markDrop(into);
633
+ const over = nodeAt(event.target);
634
+ spring(over?.dataset.type === 'dir' ? over.dataset.path : null);
635
+ });
636
+ el.addEventListener('dragleave', (event) => { if (!el.contains(event.relatedTarget)) { markDrop(null); spring(null); } });
637
+ el.addEventListener('drop', (event) => {
638
+ const into = dropInto;
639
+ markDrop(null);
640
+ spring(null);
641
+ if (!into) return;
642
+ event.preventDefault();
643
+ if (dragging) hooks.onDrop({ rows: dragging, into, copy: mac ? event.altKey : event.ctrlKey });
644
+ else hooks.onDrop({ transfer: event.dataTransfer, into });
645
+ });
646
+ el.addEventListener('dragend', () => {
647
+ dragging = null;
648
+ markDrop(null);
649
+ spring(null);
650
+ el.classList.remove('fe-tree--dragging');
651
+ hooks.onDragEnd();
652
+ });
653
+
654
+ // ── Keyboard ──
655
+
656
+ function move(to, { extend = false, keep = false } = {}) {
657
+ if (!to) return;
658
+ if (extend) {
659
+ const from = anchor && nodes.has(anchor) ? anchor : (isStop(focus) ? null : focus) || to;
660
+ apply({ selected: new Set(rangeBetween(order({ withStops: true }), from, to)), anchor: from, focus: to });
661
+ } else if (keep || isStop(to)) apply({ selected, anchor, focus: to });
662
+ else apply({ selected: new Set([to]), anchor: to, focus: to });
663
+ }
664
+
665
+ // The keyboard is on a line that offers something: the arrows move on from it, Enter or Space takes the offer,
666
+ // and what is about files (rename, delete, paste) has nothing to act on.
667
+ function onStopKey(what, rows) {
668
+ const line = stops.get(focus);
669
+ switch (what.action) {
670
+ case 'down': case 'up': case 'home': case 'end': case 'pageDown': case 'pageUp':
671
+ move(moveTarget(rows, focus, what.action, { page: Math.max(1, Math.floor(el.clientHeight / 24) - 1) }), { keep: what.keep });
672
+ return;
673
+ case 'left': { const dir = line?.dataset.dir; if (nodes.has(dir)) move(dir); return; }
674
+ case 'open': case 'peek': useStop(focus); return;
675
+ case 'escape': hooks.onAction('escape', { paths: topLevelOf([...selected]), focus: null }); return;
676
+ default:
677
+ }
678
+ }
679
+
680
+ function onKey(event) {
681
+ if (editing) return false;
682
+ const what = keyAction(event, { mac });
683
+ if (!what) return false;
684
+ const rows = order({ withStops: true });
685
+ if (isStop(focus) && !what.type) { onStopKey(what, rows); return true; }
686
+ const node = focus ? nodes.get(focus) : null;
687
+ if (what.type) {
688
+ typed += what.type;
689
+ clearTimeout(typedTimer);
690
+ typedTimer = setTimeout(() => { typed = ''; }, TYPE_AHEAD_MS);
691
+ const to = typeAheadTarget(rows, (path) => nodes.get(path)?.dataset.name || '', focus, typed);
692
+ if (to) move(to);
693
+ return true;
694
+ }
695
+ switch (what.action) {
696
+ case 'down': case 'up': case 'home': case 'end': case 'pageDown': case 'pageUp':
697
+ move(moveTarget(rows, focus, what.action, { page: Math.max(1, Math.floor(el.clientHeight / 24) - 1) }), what);
698
+ return true;
699
+ case 'right':
700
+ if (node?.dataset.type === 'dir') {
701
+ if (node.dataset.expanded !== 'true') hooks.onToggle(focus, true);
702
+ else { const first = node.querySelector(':scope > .fe-children > .fe-node:not([data-temp])'); if (first) move(first.dataset.path); }
703
+ }
704
+ return true;
705
+ case 'left':
706
+ if (node?.dataset.type === 'dir' && node.dataset.expanded === 'true') hooks.onToggle(focus, false);
707
+ else if (node) { const up = dirName(focus); if (nodes.has(up)) move(up); }
708
+ return true;
709
+ case 'open': case 'peek':
710
+ if (!node) return true;
711
+ if (node.dataset.type === 'dir') hooks.onToggle(focus, node.dataset.expanded !== 'true');
712
+ // Enter opens a file to work on it (its tab stays, the editor takes the keyboard); Space only looks at it.
713
+ else hooks.onOpen(focus, node._item, what.action === 'open' ? { pin: true } : { pin: false, keep: true });
714
+ return true;
715
+ case 'toggle':
716
+ if (focus) apply(pickRow({ selected, anchor }, focus, { toggle: true }, rows));
717
+ return true;
718
+ case 'selectAll': {
719
+ const all = rows.filter((path) => !isStop(path));
720
+ apply({ selected: new Set(all), anchor: all[0] || null, focus: focus || all[0] || null }, { scroll: false });
721
+ return true;
722
+ }
723
+ case 'menu':
724
+ menuFor(node, { keyboard: true });
725
+ return true;
726
+ default:
727
+ hooks.onAction(what.action, { paths: topLevelOf([...selected]), focus });
728
+ return true;
729
+ }
730
+ }
731
+
732
+ // Taken at the window while the tree has the keyboard, before the page's own shortcuts: the whiteboard listens
733
+ // at the document in the capture phase, and a letter typed for a name is not one of the app's keys.
734
+ const keys = (event) => {
735
+ if (document.activeElement !== el) return;
736
+ if (event.type === 'keydown') { if (onKey(event)) { event.preventDefault(); event.stopPropagation(); } return; }
737
+ if (keyAction(event, { mac })) event.stopPropagation();
738
+ };
739
+ for (const type of ['keydown', 'keyup', 'keypress']) window.addEventListener(type, keys, true);
740
+
741
+ el.addEventListener('focus', () => {
742
+ if (focus && there(focus)) return;
743
+ const first = [...selected].find((path) => nodes.has(path)) || order()[0] || null;
744
+ if (first) apply({ selected: selected.size ? selected : new Set([first]), anchor: anchor || first, focus: first }, { scroll: false });
745
+ });
746
+ let scrollFrame = 0;
747
+ el.addEventListener('scroll', () => { if (!scrollFrame) scrollFrame = requestAnimationFrame(() => { scrollFrame = 0; hooks.onScroll(el.scrollTop); }); }, { passive: true });
748
+
749
+ return {
750
+ /** Show another project's folder: everything that was drawn goes. */
751
+ setRoot(path) {
752
+ endEdit();
753
+ for (const node of [...nodes.values()]) node.remove();
754
+ nodes.clear(); stops.clear(); shown.clear();
755
+ el.textContent = '';
756
+ selected = new Set(); anchor = null; focus = null; wantFirst = false;
757
+ root = path;
758
+ markFocus();
759
+ },
760
+ render,
761
+ renderDir,
762
+ refreshRows,
763
+ order,
764
+ selection,
765
+ select,
766
+ has: (path) => nodes.has(path),
767
+ itemOf: (path) => nodes.get(path)?._item || null,
768
+ rowOf,
769
+ isEditing: () => !!editing,
770
+ startEdit,
771
+ endEdit,
772
+ settleEdit,
773
+ focus: () => el.focus({ preventScroll: true }),
774
+ /** Mark a row for a moment (a file that was just revealed). */
775
+ flash(path) {
776
+ const row = rowOf(path);
777
+ if (!row) return;
778
+ row.scrollIntoView({ block: 'center' });
779
+ row.classList.add('fe-row--ctx-active');
780
+ setTimeout(() => row.classList.remove('fe-row--ctx-active'), 1600);
781
+ },
782
+ scrollTop: () => el.scrollTop,
783
+ setScrollTop(top) { el.scrollTop = top; },
784
+ destroy() {
785
+ for (const type of ['keydown', 'keyup', 'keypress']) window.removeEventListener(type, keys, true);
786
+ more?.disconnect();
787
+ clearTimeout(springTimer); clearTimeout(typedTimer); clearTimeout(pressTimer);
788
+ },
789
+ };
790
+ }