synabun 2.0.0-beta → 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 (374) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/NOTICE +5 -0
  3. package/README.md +1 -1
  4. package/THIRD-PARTY-LICENSES.md +51 -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/launcher.mjs +40 -0
  14. package/lib/claude-hooks.js +61 -8
  15. package/lib/client-config-repair.js +4 -3
  16. package/lib/data-home-migration.js +4 -0
  17. package/lib/desktop-pwa.js +206 -0
  18. package/lib/external-tools.js +221 -0
  19. package/lib/first-launch-protection.js +5 -0
  20. package/lib/packaged-runtime.js +110 -0
  21. package/lib/start-launcher.js +1396 -0
  22. package/mcp-server/package-lock.json +2 -2
  23. package/mcp-server/package.json +1 -3
  24. package/neural-interface/i18n/en.json +1540 -154
  25. package/neural-interface/i18n/pt-BR.json +1538 -153
  26. package/neural-interface/lib/assistant-api.js +62 -6
  27. package/neural-interface/lib/assistant-brains/claude.js +4 -0
  28. package/neural-interface/lib/assistant-catalog.js +179 -27
  29. package/neural-interface/lib/assistant-config.js +31 -2
  30. package/neural-interface/lib/assistant-dispatch.js +62 -13
  31. package/neural-interface/lib/assistant-envelope.js +9 -6
  32. package/neural-interface/lib/assistant-hidden-models.js +393 -66
  33. package/neural-interface/lib/assistant-memory.js +24 -10
  34. package/neural-interface/lib/assistant-persona.js +6 -2
  35. package/neural-interface/lib/assistant-pricing.js +21 -6
  36. package/neural-interface/lib/assistant-runtime.js +181 -12
  37. package/neural-interface/lib/assistant-task-prompt.js +13 -3
  38. package/neural-interface/lib/assistant-test-density.js +142 -0
  39. package/neural-interface/lib/assistant-user-context-budgets.js +123 -0
  40. package/neural-interface/lib/assistant-user-context.js +908 -0
  41. package/neural-interface/lib/assistant-user-knowledge.js +328 -0
  42. package/neural-interface/lib/assistant-user-learn.js +404 -0
  43. package/neural-interface/lib/assistant-user-threads.js +226 -0
  44. package/neural-interface/lib/assistant-user-writer.js +181 -0
  45. package/neural-interface/lib/augmented-path.js +23 -3
  46. package/neural-interface/lib/browser-background-tab.js +134 -0
  47. package/neural-interface/lib/browser-language.js +83 -0
  48. package/neural-interface/lib/browser-screenshot.js +678 -61
  49. package/neural-interface/lib/claude-agent-bridge.js +87 -153
  50. package/neural-interface/lib/claude-executable.js +241 -0
  51. package/neural-interface/lib/claude-model-catalog.js +82 -93
  52. package/neural-interface/lib/claude-temporary.js +46 -7
  53. package/neural-interface/lib/cloudflared/install.js +533 -0
  54. package/neural-interface/lib/cloudflared/manager.js +545 -0
  55. package/neural-interface/lib/cloudflared/pins.js +115 -0
  56. package/neural-interface/lib/cloudflared/pins.json +63 -0
  57. package/neural-interface/lib/cloudflared/tunnel.js +265 -0
  58. package/neural-interface/lib/editor-completion-endpoint.js +264 -0
  59. package/neural-interface/lib/editor-completion-opencode.js +436 -0
  60. package/neural-interface/lib/editor-completion.js +714 -0
  61. package/neural-interface/lib/editor-config.js +149 -0
  62. package/neural-interface/lib/editor-files-api.js +268 -0
  63. package/neural-interface/lib/editor-git-api.js +134 -0
  64. package/neural-interface/lib/file-explorer-api.js +1039 -0
  65. package/neural-interface/lib/file-explorer-system.js +355 -0
  66. package/neural-interface/lib/file-explorer-trash.ps1 +85 -0
  67. package/neural-interface/lib/file-explorer-watch.js +239 -0
  68. package/neural-interface/lib/git-diff-api.js +103 -0
  69. package/neural-interface/lib/git-read.js +202 -0
  70. package/neural-interface/lib/greeting/api.js +89 -0
  71. package/neural-interface/lib/greeting/budgets.js +90 -0
  72. package/neural-interface/lib/greeting/engine.js +461 -0
  73. package/neural-interface/lib/greeting/facts.js +320 -0
  74. package/neural-interface/lib/greeting/sources.js +252 -0
  75. package/neural-interface/lib/greeting/writer.js +139 -0
  76. package/neural-interface/lib/http-guards.js +189 -13
  77. package/neural-interface/lib/machine-names.js +88 -0
  78. package/neural-interface/lib/memory-api.js +31 -7
  79. package/neural-interface/lib/native-binary-runtime.js +69 -337
  80. package/neural-interface/lib/native-loop-providers.js +40 -49
  81. package/neural-interface/lib/offline-page.js +54 -0
  82. package/neural-interface/lib/plugin-installer.js +4 -2
  83. package/neural-interface/lib/project-storage.js +5 -7
  84. package/neural-interface/lib/request-gate.js +589 -0
  85. package/neural-interface/lib/session-title-generator.js +5 -0
  86. package/neural-interface/lib/synabun-update-plan.js +19 -0
  87. package/neural-interface/lib/ui-blob-api.js +108 -0
  88. package/neural-interface/lib/ui-blob-store.js +68 -0
  89. package/neural-interface/package-lock.json +701 -253
  90. package/neural-interface/package.json +13 -2
  91. package/neural-interface/public/install-app.html +34 -0
  92. package/neural-interface/public/manifest.json +2 -0
  93. package/neural-interface/public/offline.html +402 -66
  94. package/neural-interface/public/onboarding.html +264 -147
  95. package/neural-interface/public/shared/agent-runtime-options.js +22 -6
  96. package/neural-interface/public/shared/assistant/asst-automation.js +286 -0
  97. package/neural-interface/public/shared/assistant/asst-brain-picker.js +312 -62
  98. package/neural-interface/public/shared/assistant/asst-composer.js +11 -1
  99. package/neural-interface/public/shared/assistant/asst-mascot.js +26 -64
  100. package/neural-interface/public/shared/assistant/asst-menu.js +12 -8
  101. package/neural-interface/public/shared/assistant/asst-panel.js +150 -28
  102. package/neural-interface/public/shared/assistant/asst-rack-styles.js +0 -3
  103. package/neural-interface/public/shared/assistant/asst-render.js +219 -67
  104. package/neural-interface/public/shared/assistant/asst-route-models.js +108 -0
  105. package/neural-interface/public/shared/assistant/asst-route.js +23 -329
  106. package/neural-interface/public/shared/assistant/asst-routes-editor.js +495 -0
  107. package/neural-interface/public/shared/assistant/asst-sanitize.js +8 -1
  108. package/neural-interface/public/shared/assistant/asst-sidepanel.js +31 -9
  109. package/neural-interface/public/shared/assistant/asst-state.js +185 -1
  110. package/neural-interface/public/shared/assistant/asst-styles.js +162 -6
  111. package/neural-interface/public/shared/blob-store.js +111 -22
  112. package/neural-interface/public/shared/cdx/cdx-panel.js +32 -11
  113. package/neural-interface/public/shared/cdx/cdx-render.js +25 -89
  114. package/neural-interface/public/shared/cdx/cdx-styles.js +36 -0
  115. package/neural-interface/public/shared/cdx/cdx-tabs.js +87 -4
  116. package/neural-interface/public/shared/command-runner-model.js +30 -0
  117. package/neural-interface/public/shared/constants.js +6 -1
  118. package/neural-interface/public/shared/cp/cp-rehydrate.js +2 -1
  119. package/neural-interface/public/shared/cp/cp-styles.js +15 -0
  120. package/neural-interface/public/shared/editor/editor-ai-logic.js +409 -0
  121. package/neural-interface/public/shared/editor/editor-ai.js +619 -0
  122. package/neural-interface/public/shared/editor/editor-commands.js +212 -0
  123. package/neural-interface/public/shared/editor/editor-compare.js +103 -0
  124. package/neural-interface/public/shared/editor/editor-completion-list.js +172 -0
  125. package/neural-interface/public/shared/editor/editor-doc.js +257 -0
  126. package/neural-interface/public/shared/editor/editor-git-logic.js +131 -0
  127. package/neural-interface/public/shared/editor/editor-git.js +95 -0
  128. package/neural-interface/public/shared/editor/editor-languages.js +156 -0
  129. package/neural-interface/public/shared/editor/editor-lint.js +229 -0
  130. package/neural-interface/public/shared/editor/editor-loader.js +51 -0
  131. package/neural-interface/public/shared/editor/editor-markdown.js +90 -0
  132. package/neural-interface/public/shared/editor/editor-menu.js +233 -0
  133. package/neural-interface/public/shared/editor/editor-minimap.js +371 -0
  134. package/neural-interface/public/shared/editor/editor-palette-logic.js +192 -0
  135. package/neural-interface/public/shared/editor/editor-palette.js +125 -0
  136. package/neural-interface/public/shared/editor/editor-panel-logic.js +288 -0
  137. package/neural-interface/public/shared/editor/editor-panel.js +2099 -0
  138. package/neural-interface/public/shared/editor/editor-prefs.js +224 -0
  139. package/neural-interface/public/shared/editor/editor-safety.js +500 -0
  140. package/neural-interface/public/shared/editor/editor-search.js +352 -0
  141. package/neural-interface/public/shared/editor/editor-statusbar.js +144 -0
  142. package/neural-interface/public/shared/editor/editor-tabs-logic.js +331 -0
  143. package/neural-interface/public/shared/editor/editor-tabs.js +233 -0
  144. package/neural-interface/public/shared/editor/editor-theme.js +244 -0
  145. package/neural-interface/public/shared/editor/editor-view.js +332 -0
  146. package/neural-interface/public/shared/explorer/fx-api.js +216 -0
  147. package/neural-interface/public/shared/explorer/fx-links.js +204 -0
  148. package/neural-interface/public/shared/explorer/fx-logic.js +537 -0
  149. package/neural-interface/public/shared/explorer/fx-main.js +865 -0
  150. package/neural-interface/public/shared/explorer/fx-names.js +87 -0
  151. package/neural-interface/public/shared/explorer/fx-ops.js +370 -0
  152. package/neural-interface/public/shared/explorer/fx-popups.js +490 -0
  153. package/neural-interface/public/shared/explorer/fx-tree.js +790 -0
  154. package/neural-interface/public/shared/file-links.js +339 -0
  155. package/neural-interface/public/shared/greeting/greeting-card.js +178 -0
  156. package/neural-interface/public/shared/greeting/greeting-view.js +148 -0
  157. package/neural-interface/public/shared/html-shell.js +169 -170
  158. package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -0
  159. package/neural-interface/public/shared/ocp-v2/ocp-v2-greeting.js +47 -0
  160. package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +99 -17
  161. package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
  162. package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +5 -0
  163. package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +40 -0
  164. package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +3 -2
  165. package/neural-interface/public/shared/pwa-install-page.js +62 -0
  166. package/neural-interface/public/shared/pwa-install.css +12 -0
  167. package/neural-interface/public/shared/pwa-install.js +127 -0
  168. package/neural-interface/public/shared/settings/settings-code-editor-pane.js +412 -0
  169. package/neural-interface/public/shared/settings/settings-code-editor.js +328 -0
  170. package/neural-interface/public/shared/settings/settings-greeting.js +133 -0
  171. package/neural-interface/public/shared/settings/settings-ia.js +18 -4
  172. package/neural-interface/public/shared/settings/settings-rules.js +160 -0
  173. package/neural-interface/public/shared/settings/settings-user-context.js +154 -0
  174. package/neural-interface/public/shared/start-bridge.js +802 -0
  175. package/neural-interface/public/shared/styleguide/sg-colors.js +5 -2
  176. package/neural-interface/public/shared/styleguide/sg-import-export.js +4 -3
  177. package/neural-interface/public/shared/styleguide/sg-kit.js +4 -1
  178. package/neural-interface/public/shared/styleguide/sg-panel.js +9 -1
  179. package/neural-interface/public/shared/styleguide/sg-typography.js +1 -0
  180. package/neural-interface/public/shared/styles.css +2219 -1117
  181. package/neural-interface/public/shared/synabun-mascot.js +20 -56
  182. package/neural-interface/public/shared/term-ready.js +307 -0
  183. package/neural-interface/public/shared/tunnel-view.js +167 -0
  184. package/neural-interface/public/shared/ui-claude-panel.js +160 -19
  185. package/neural-interface/public/shared/ui-command-runner.js +251 -468
  186. package/neural-interface/public/shared/ui-detail.js +15 -0
  187. package/neural-interface/public/shared/ui-explorer.js +22 -0
  188. package/neural-interface/public/shared/ui-file-explorer.js +481 -2496
  189. package/neural-interface/public/shared/ui-file-links.js +103 -0
  190. package/neural-interface/public/shared/ui-help.js +26 -0
  191. package/neural-interface/public/shared/ui-invite.js +126 -116
  192. package/neural-interface/public/shared/ui-judgments.js +1 -1
  193. package/neural-interface/public/shared/ui-keybinds.js +4 -0
  194. package/neural-interface/public/shared/ui-loading.js +367 -12
  195. package/neural-interface/public/shared/ui-menubar.js +21 -55
  196. package/neural-interface/public/shared/ui-native-loop-router-state.js +11 -0
  197. package/neural-interface/public/shared/ui-native-loop-router.js +15 -2
  198. package/neural-interface/public/shared/ui-navbar.js +45 -21
  199. package/neural-interface/public/shared/ui-settings.js +491 -547
  200. package/neural-interface/public/shared/ui-sidepanel-runtime.js +2 -1
  201. package/neural-interface/public/shared/ui-sync.js +16 -0
  202. package/neural-interface/public/shared/ui-terminal.js +153 -149
  203. package/neural-interface/public/shared/ui-tunnel.js +130 -0
  204. package/neural-interface/public/shared/ui-tutorial-steps.js +6 -2
  205. package/neural-interface/public/shared/ui-whiteboard.js +12 -0
  206. package/neural-interface/public/shared/ui-worker-end.js +245 -0
  207. package/neural-interface/public/shared/ui-workspaces.js +41 -27
  208. package/neural-interface/public/shared/worker-end.js +251 -0
  209. package/neural-interface/public/sw.js +3 -1
  210. package/neural-interface/public/variant/2d/main.js +2 -1
  211. package/neural-interface/public/variant/3d/main.js +2 -1
  212. package/neural-interface/public/vendor/codemirror/chunks/apl-CSDKT72Z.js +1 -0
  213. package/neural-interface/public/vendor/codemirror/chunks/asciiarmor-JYYPU5BM.js +1 -0
  214. package/neural-interface/public/vendor/codemirror/chunks/asn1-NRWCZDA6.js +1 -0
  215. package/neural-interface/public/vendor/codemirror/chunks/asterisk-5OU6D4ZQ.js +1 -0
  216. package/neural-interface/public/vendor/codemirror/chunks/brainfuck-ID4N4SYC.js +1 -0
  217. package/neural-interface/public/vendor/codemirror/chunks/chunk-4RZWGCS2.js +1 -0
  218. package/neural-interface/public/vendor/codemirror/chunks/chunk-ACTJM3JR.js +11 -0
  219. package/neural-interface/public/vendor/codemirror/chunks/chunk-S6BMAXLK.js +1 -0
  220. package/neural-interface/public/vendor/codemirror/chunks/chunk-TO2RDOHQ.js +3 -0
  221. package/neural-interface/public/vendor/codemirror/chunks/chunk-US2GMZZQ.js +13 -0
  222. package/neural-interface/public/vendor/codemirror/chunks/chunk-WYYO5ZAJ.js +1 -0
  223. package/neural-interface/public/vendor/codemirror/chunks/chunk-XALZTDJC.js +1 -0
  224. package/neural-interface/public/vendor/codemirror/chunks/chunk-YRNUTRDV.js +1 -0
  225. package/neural-interface/public/vendor/codemirror/chunks/chunk-Z3QOKH62.js +1 -0
  226. package/neural-interface/public/vendor/codemirror/chunks/clike-OWQBSOGL.js +1 -0
  227. package/neural-interface/public/vendor/codemirror/chunks/clojure-KV75UQWC.js +1 -0
  228. package/neural-interface/public/vendor/codemirror/chunks/cmake-GTEP5Y3C.js +1 -0
  229. package/neural-interface/public/vendor/codemirror/chunks/cobol-WCZ7QEGJ.js +1 -0
  230. package/neural-interface/public/vendor/codemirror/chunks/coffeescript-TGLBOT6B.js +1 -0
  231. package/neural-interface/public/vendor/codemirror/chunks/commonlisp-WVVI432V.js +1 -0
  232. package/neural-interface/public/vendor/codemirror/chunks/crystal-S6R6HLKQ.js +1 -0
  233. package/neural-interface/public/vendor/codemirror/chunks/css-PDA2RINP.js +1 -0
  234. package/neural-interface/public/vendor/codemirror/chunks/cypher-HL5QPL4C.js +1 -0
  235. package/neural-interface/public/vendor/codemirror/chunks/d-HHJH2HAJ.js +1 -0
  236. package/neural-interface/public/vendor/codemirror/chunks/diff-MN3A35BY.js +1 -0
  237. package/neural-interface/public/vendor/codemirror/chunks/dist-25SQLBVS.js +3 -0
  238. package/neural-interface/public/vendor/codemirror/chunks/dist-27ZGGJJS.js +1 -0
  239. package/neural-interface/public/vendor/codemirror/chunks/dist-4E52OJIF.js +1 -0
  240. package/neural-interface/public/vendor/codemirror/chunks/dist-6FT73XDC.js +2 -0
  241. package/neural-interface/public/vendor/codemirror/chunks/dist-BE6YGRMW.js +1 -0
  242. package/neural-interface/public/vendor/codemirror/chunks/dist-CJSGKW2O.js +1 -0
  243. package/neural-interface/public/vendor/codemirror/chunks/dist-CRPH2WLI.js +1 -0
  244. package/neural-interface/public/vendor/codemirror/chunks/dist-CTG7JEQN.js +1 -0
  245. package/neural-interface/public/vendor/codemirror/chunks/dist-I3FEVELX.js +1 -0
  246. package/neural-interface/public/vendor/codemirror/chunks/dist-IM5BBSWZ.js +1 -0
  247. package/neural-interface/public/vendor/codemirror/chunks/dist-KERUKTIF.js +1 -0
  248. package/neural-interface/public/vendor/codemirror/chunks/dist-LXTMWZAM.js +1 -0
  249. package/neural-interface/public/vendor/codemirror/chunks/dist-MABASSET.js +1 -0
  250. package/neural-interface/public/vendor/codemirror/chunks/dist-MJVBUZLL.js +3 -0
  251. package/neural-interface/public/vendor/codemirror/chunks/dist-MNB3WZVU.js +1 -0
  252. package/neural-interface/public/vendor/codemirror/chunks/dist-QYWXOE3U.js +7 -0
  253. package/neural-interface/public/vendor/codemirror/chunks/dist-TEBOFWLZ.js +1 -0
  254. package/neural-interface/public/vendor/codemirror/chunks/dist-UNXFROM3.js +1 -0
  255. package/neural-interface/public/vendor/codemirror/chunks/dist-XL2SLQO6.js +1 -0
  256. package/neural-interface/public/vendor/codemirror/chunks/dist-XO42Y3AP.js +6 -0
  257. package/neural-interface/public/vendor/codemirror/chunks/dist-Y2WWDWJK.js +1 -0
  258. package/neural-interface/public/vendor/codemirror/chunks/dist-YFEZKWMU.js +1 -0
  259. package/neural-interface/public/vendor/codemirror/chunks/dist-ZY7SWQO6.js +1 -0
  260. package/neural-interface/public/vendor/codemirror/chunks/dockerfile-3Y4R4NCH.js +1 -0
  261. package/neural-interface/public/vendor/codemirror/chunks/dtd-5343GJ4P.js +1 -0
  262. package/neural-interface/public/vendor/codemirror/chunks/dylan-FI2CXSW2.js +1 -0
  263. package/neural-interface/public/vendor/codemirror/chunks/ebnf-GNQ77YWA.js +1 -0
  264. package/neural-interface/public/vendor/codemirror/chunks/ecl-MRPUAE2L.js +1 -0
  265. package/neural-interface/public/vendor/codemirror/chunks/eiffel-DZROES27.js +1 -0
  266. package/neural-interface/public/vendor/codemirror/chunks/elm-4KICRFKQ.js +1 -0
  267. package/neural-interface/public/vendor/codemirror/chunks/erlang-3IHYC263.js +1 -0
  268. package/neural-interface/public/vendor/codemirror/chunks/factor-C6D3E5UK.js +1 -0
  269. package/neural-interface/public/vendor/codemirror/chunks/fcl-VYTWUJMI.js +1 -0
  270. package/neural-interface/public/vendor/codemirror/chunks/forth-ZBO2EH5M.js +1 -0
  271. package/neural-interface/public/vendor/codemirror/chunks/fortran-5NDCRWIK.js +1 -0
  272. package/neural-interface/public/vendor/codemirror/chunks/gas-SKRMOXFP.js +1 -0
  273. package/neural-interface/public/vendor/codemirror/chunks/gherkin-L45YXZLW.js +1 -0
  274. package/neural-interface/public/vendor/codemirror/chunks/groovy-GLPTLN44.js +1 -0
  275. package/neural-interface/public/vendor/codemirror/chunks/haskell-E7NF7P4Y.js +1 -0
  276. package/neural-interface/public/vendor/codemirror/chunks/haxe-K2NVDOB6.js +1 -0
  277. package/neural-interface/public/vendor/codemirror/chunks/http-Q6DRQIJB.js +1 -0
  278. package/neural-interface/public/vendor/codemirror/chunks/idl-MZYNJVLN.js +1 -0
  279. package/neural-interface/public/vendor/codemirror/chunks/javascript-6YRYK2HR.js +1 -0
  280. package/neural-interface/public/vendor/codemirror/chunks/julia-HXEL2T5G.js +1 -0
  281. package/neural-interface/public/vendor/codemirror/chunks/livescript-MV7CI2JZ.js +1 -0
  282. package/neural-interface/public/vendor/codemirror/chunks/lua-E36NHOOD.js +1 -0
  283. package/neural-interface/public/vendor/codemirror/chunks/mathematica-3XOZZCMC.js +1 -0
  284. package/neural-interface/public/vendor/codemirror/chunks/mbox-ODYGFKS7.js +1 -0
  285. package/neural-interface/public/vendor/codemirror/chunks/mirc-WB43AWF5.js +1 -0
  286. package/neural-interface/public/vendor/codemirror/chunks/mllike-ZT6ET7ZH.js +1 -0
  287. package/neural-interface/public/vendor/codemirror/chunks/modelica-LLMZPFGA.js +1 -0
  288. package/neural-interface/public/vendor/codemirror/chunks/mscgen-2OPNNYFH.js +1 -0
  289. package/neural-interface/public/vendor/codemirror/chunks/mumps-6P3HWJH6.js +1 -0
  290. package/neural-interface/public/vendor/codemirror/chunks/nginx-TCGKQTRW.js +1 -0
  291. package/neural-interface/public/vendor/codemirror/chunks/nsis-ATIGIXTJ.js +1 -0
  292. package/neural-interface/public/vendor/codemirror/chunks/ntriples-5PLBI5OV.js +1 -0
  293. package/neural-interface/public/vendor/codemirror/chunks/octave-QF2S3NL6.js +1 -0
  294. package/neural-interface/public/vendor/codemirror/chunks/oz-JFIVFFH2.js +1 -0
  295. package/neural-interface/public/vendor/codemirror/chunks/pascal-JG6SW76V.js +1 -0
  296. package/neural-interface/public/vendor/codemirror/chunks/perl-4B3IWVVO.js +1 -0
  297. package/neural-interface/public/vendor/codemirror/chunks/pig-ABEBH4MI.js +1 -0
  298. package/neural-interface/public/vendor/codemirror/chunks/powershell-H32XE7TL.js +1 -0
  299. package/neural-interface/public/vendor/codemirror/chunks/properties-NN644MMA.js +1 -0
  300. package/neural-interface/public/vendor/codemirror/chunks/protobuf-YKWQK2J3.js +1 -0
  301. package/neural-interface/public/vendor/codemirror/chunks/pug-RIYXEKFW.js +1 -0
  302. package/neural-interface/public/vendor/codemirror/chunks/puppet-JZQ5WPEV.js +1 -0
  303. package/neural-interface/public/vendor/codemirror/chunks/python-EBCGGHAC.js +1 -0
  304. package/neural-interface/public/vendor/codemirror/chunks/q-5UMH2YQY.js +1 -0
  305. package/neural-interface/public/vendor/codemirror/chunks/r-7XMGG453.js +1 -0
  306. package/neural-interface/public/vendor/codemirror/chunks/rpm-IEV7TSLR.js +1 -0
  307. package/neural-interface/public/vendor/codemirror/chunks/ruby-T4SPFPNM.js +1 -0
  308. package/neural-interface/public/vendor/codemirror/chunks/sas-GEDX67ET.js +1 -0
  309. package/neural-interface/public/vendor/codemirror/chunks/scheme-FTLOWKPC.js +1 -0
  310. package/neural-interface/public/vendor/codemirror/chunks/shell-HUFRKFSJ.js +1 -0
  311. package/neural-interface/public/vendor/codemirror/chunks/sieve-P5QUDVYO.js +1 -0
  312. package/neural-interface/public/vendor/codemirror/chunks/smalltalk-2SORJNQI.js +1 -0
  313. package/neural-interface/public/vendor/codemirror/chunks/solr-M22TGAR5.js +1 -0
  314. package/neural-interface/public/vendor/codemirror/chunks/sparql-4M3KL752.js +1 -0
  315. package/neural-interface/public/vendor/codemirror/chunks/spreadsheet-3K23LA4N.js +1 -0
  316. package/neural-interface/public/vendor/codemirror/chunks/sql-VHRFMUBI.js +1 -0
  317. package/neural-interface/public/vendor/codemirror/chunks/stex-QZDAQUY2.js +1 -0
  318. package/neural-interface/public/vendor/codemirror/chunks/stylus-7ZDKAKBD.js +1 -0
  319. package/neural-interface/public/vendor/codemirror/chunks/swift-WCLCLGMR.js +1 -0
  320. package/neural-interface/public/vendor/codemirror/chunks/tcl-JLMU6WOK.js +1 -0
  321. package/neural-interface/public/vendor/codemirror/chunks/textile-4CHDKZ4T.js +1 -0
  322. package/neural-interface/public/vendor/codemirror/chunks/tiddlywiki-DKMCZZQ4.js +1 -0
  323. package/neural-interface/public/vendor/codemirror/chunks/tiki-F4JIG36Q.js +1 -0
  324. package/neural-interface/public/vendor/codemirror/chunks/toml-BHMHTIM2.js +1 -0
  325. package/neural-interface/public/vendor/codemirror/chunks/troff-YQ2KV5NT.js +1 -0
  326. package/neural-interface/public/vendor/codemirror/chunks/ttcn-ZHRCVFJY.js +1 -0
  327. package/neural-interface/public/vendor/codemirror/chunks/ttcn-cfg-KJOVTY4X.js +1 -0
  328. package/neural-interface/public/vendor/codemirror/chunks/turtle-535P5N6I.js +1 -0
  329. package/neural-interface/public/vendor/codemirror/chunks/vb-76UMQHOT.js +1 -0
  330. package/neural-interface/public/vendor/codemirror/chunks/vbscript-CGNDTK6Z.js +1 -0
  331. package/neural-interface/public/vendor/codemirror/chunks/velocity-VSJDO5T5.js +1 -0
  332. package/neural-interface/public/vendor/codemirror/chunks/verilog-4T6ETXIP.js +1 -0
  333. package/neural-interface/public/vendor/codemirror/chunks/vhdl-T7FNAXUN.js +1 -0
  334. package/neural-interface/public/vendor/codemirror/chunks/webidl-P6MJSJ6J.js +1 -0
  335. package/neural-interface/public/vendor/codemirror/chunks/xquery-UOVILRGN.js +1 -0
  336. package/neural-interface/public/vendor/codemirror/chunks/yacas-CV2APROL.js +1 -0
  337. package/neural-interface/public/vendor/codemirror/chunks/z80-3KFNVM4M.js +1 -0
  338. package/neural-interface/public/vendor/codemirror/codemirror-6.43.14-75f05e67.min.js +2 -0
  339. package/neural-interface/public/vendor/codemirror/manifest.json +60 -0
  340. package/neural-interface/scripts/build-vendor.mjs +78 -2
  341. package/neural-interface/scripts/rebuild-pty.js +15 -12
  342. package/neural-interface/scripts/vendor-codemirror-entry.js +121 -0
  343. package/neural-interface/server.js +1092 -1212
  344. package/neural-interface/templates/rulesets/README.md +1 -1
  345. package/package.json +13 -7
  346. package/postinstall.js +11 -81
  347. package/setup.js +111 -6
  348. package/benchmarks/browser-live-results.json +0 -79
  349. package/benchmarks/browser-v2-results.json +0 -258
  350. package/docs/browser-v2.md +0 -109
  351. package/docs/hook-integrity.md +0 -89
  352. package/docs/judgments.md +0 -182
  353. package/docs/style-guide.md +0 -246
  354. package/neural-interface/lib/codex-capabilities.d.ts +0 -41
  355. package/neural-interface/public/category-logos/claude.svg +0 -1
  356. package/neural-interface/public/category-logos/gemini-color.svg +0 -1
  357. package/neural-interface/public/category-logos/openai.svg +0 -1
  358. package/neural-interface/public/category-logos/synabun.png +0 -0
  359. package/neural-interface/public/claude.svg +0 -1
  360. package/neural-interface/public/gallery.svg +0 -6
  361. package/neural-interface/public/leonardoai.svg +0 -1
  362. package/neural-interface/public/mcp.svg +0 -4
  363. package/neural-interface/public/opencode-logo-dark-square.svg +0 -1
  364. package/neural-interface/public/pin-svgrepo-com.svg +0 -25
  365. package/neural-interface/public/sessionicon.svg +0 -8
  366. package/neural-interface/public/toolbar-album.svg +0 -8
  367. package/neural-interface/public/toolbar-question.svg +0 -6
  368. package/neural-interface/public/toolbar-sessions.svg +0 -6
  369. package/neural-interface/scripts/codex-protocol-fixture.mjs +0 -83
  370. package/scripts/audit-claude-hook-usage.mjs +0 -12
  371. package/scripts/audit-memory-duplicates.mjs +0 -44
  372. package/scripts/measure-claude-hook-overhead.mjs +0 -77
  373. package/scripts/recover-user-config.mjs +0 -254
  374. package/skills/synabun/synabunicon.png +0 -0
@@ -0,0 +1,865 @@
1
+ // ═══════════════════════════════════════════
2
+ // SynaBun Neural Interface — File Explorer: the controller
3
+ // ═══════════════════════════════════════════
4
+ //
5
+ // Holds what the explorer knows of a project (the folders that were read,
6
+ // which are open, the git state, what is cut, what the editor has open) and
7
+ // keeps the tree (fx-tree.js) true to it: it answers the tree's questions,
8
+ // acts on what the user did, follows the disk through the server's watcher,
9
+ // and remembers the tree per project. ui-file-explorer.js creates one and
10
+ // hands in what belongs to the app around it (the editor, the side panels,
11
+ // storage, the icons); this module is loaded by its address, because the
12
+ // editor's browser tests run ui-file-explorer.js without its imports.
13
+
14
+ import { t, tp } from '../i18n.js';
15
+ import { on } from '../state.js';
16
+ import { formatKey } from '../editor/editor-commands.js';
17
+ import { openEditorMenu } from '../editor/editor-menu.js';
18
+ import { createPalette } from '../editor/editor-palette.js';
19
+ import { getEditorTabs, repointEditorTab } from '../editor/editor-panel.js';
20
+ import { createExplorerApi } from './fx-api.js';
21
+ import { createEditorLink, createSender } from './fx-links.js';
22
+ import {
23
+ GIT_MARKS, baseName, buildPathTree, dirName, failureMessage, fileKind, formatBytes, gitIndex, isWithin, latestOnly, matchRange, menuModel, nameMatches,
24
+ recallTreeState, relativeTo, rememberTreeState, sameListing, sortItems, trimSlash,
25
+ } from './fx-logic.js';
26
+ import { createOps } from './fx-ops.js';
27
+ import { closeFlyout, createNotice, isFlyoutOpen, openContextMenu, openPreviewCard } from './fx-popups.js';
28
+ import { createTree } from './fx-tree.js';
29
+
30
+ export { t, tp };
31
+
32
+ const TREE_KEY = 'neural-file-explorer-tree';
33
+ const SORT_KEY = 'neural-file-explorer-sort';
34
+ const SORTS = ['name', 'date', 'size'];
35
+ const FILTER_DELAY_MS = 180;
36
+ const GIT_DELAY_MS = 250;
37
+ const GIT_MIN_INTERVAL_MS = 1500;
38
+ const DISK_MIN_INTERVAL_MS = 1000;
39
+ const PERSIST_DELAY_MS = 500;
40
+ const FOCUS_REFRESH_MS = 5000;
41
+ const EXPAND_LEVEL_MAX = 200;
42
+
43
+ /**
44
+ * @param {object} host
45
+ * @param {HTMLElement} host.panel #file-explorer-panel
46
+ * @param {{ getItem: Function, setItem: Function }} host.storage @param {object} host.KEYS
47
+ * @param {{ openFile: Function, save: Function }} host.editor shared/editor/editor-panel.js
48
+ * @param {object} host.send what each side panel offers for a path (fx-links.js createSender)
49
+ * @param {(name: string) => object} host.fileIcon @param {string} host.folderSvg
50
+ * @param {{ all: { hex: string, key: string }[], of: (path: string) => string | undefined, set: Function, move: Function }} host.colors
51
+ * @param {(git: { isGit: boolean, branch: string, count: number }) => void} host.onGit
52
+ * @param {() => void} host.addProject
53
+ * @param {typeof fetch} [host.fetchImpl]
54
+ */
55
+ export function createExplorer(host) {
56
+ const { panel, storage, KEYS = {} } = host;
57
+ const $ = (id) => document.getElementById(id);
58
+ const mac = /Mac|iPhone|iPad/.test(navigator.platform || '');
59
+ const api = createExplorerApi({ fetchImpl: host.fetchImpl });
60
+ const treeKey = KEYS.FILE_EXPLORER_TREE || TREE_KEY;
61
+ const sortKey = KEYS.FILE_EXPLORER_SORT || SORT_KEY;
62
+
63
+ const S = {
64
+ root: null, // the project's folder
65
+ label: '',
66
+ dirs: new Map(), // folder → { state: 'loading' | 'ready' | 'error', items, total, truncated, failure }
67
+ expanded: new Set(),
68
+ git: gitIndex(null),
69
+ view: null, // the filter's tree or "changed only": { kind, dirs, count, note, pending }
70
+ viewOpen: new Set(), viewClosed: new Set(),
71
+ filter: '', search: null, changedOnly: false,
72
+ sort: SORTS.includes(storage.getItem(sortKey)) ? storage.getItem(sortKey) : 'name',
73
+ clipboard: null,
74
+ visible: false,
75
+ };
76
+ let epoch = 0; // a project that was left: what it asked for is dropped when it arrives
77
+ let watcher = null;
78
+ let saidLimited = false; // this project was told once that not everything on disk is followed
79
+ let loaded = Promise.resolve();
80
+ let gitTimer = 0; let gitBusy = false; let gitAgain = false; let lastGitAt = 0;
81
+ let diskTimer = 0; let lastDiskAt = 0; const diskDue = new Set();
82
+ let filterTimer = 0; let searchAbort = null;
83
+ let persistTimer = 0; let lastFocusRefresh = 0;
84
+ let rereadTimer = 0; let lastLive = 'full';
85
+ let palette = null;
86
+ let realTop = 0; // how far the real tree was scrolled when a view took its place
87
+ const reads = latestOnly(); // which reading of each folder was asked for last
88
+
89
+ const treeEl = $('fe-tree');
90
+ const filterInput = $('fe-filter-input');
91
+ const notice = createNotice($('fe-notice'), { dismiss: t('fileExplorer.notice.dismiss') });
92
+ const rel = (path) => relativeTo(S.root, path) ?? '';
93
+ const say = (failure) => { const message = failureMessage(failure); return t(message.key, message.params); };
94
+ const revealWord = () => (api.abilities().platform === 'darwin' ? 'darwin' : api.abilities().platform === 'win32' ? 'win32' : 'other');
95
+
96
+ // ── What the tree asks ──
97
+
98
+ function itemsOf(dir) {
99
+ const below = rel(dir);
100
+ if (S.view && S.view.dirs.has(below)) {
101
+ const items = sortItems(S.view.dirs.get(below), 'name');
102
+ return { state: 'ready', items, total: items.length, truncated: false, note: below === '' ? S.view.note : '' };
103
+ }
104
+ const known = S.dirs.get(dir);
105
+ if (!known || known.state === 'loading') return { state: 'loading' };
106
+ if (known.state === 'error') return { state: 'error', message: say(known.failure) };
107
+ return { state: 'ready', items: sortItems(known.items, S.sort), total: known.total, truncated: known.truncated, note: dir === S.root ? t('fileExplorer.state.emptyProject') : '' };
108
+ }
109
+ const isExpanded = (path) => (S.view ? S.viewOpen.has(path) : S.expanded.has(path));
110
+
111
+ function rowInfo(path, item) {
112
+ const below = rel(path);
113
+ const isDir = item.type === 'dir';
114
+ const ed = editor.state();
115
+ const status = item.missing ? 'deleted' : S.git.statusOf(below, isDir);
116
+ const tone = item.missing ? 'deleted' : S.git.toneOf(below, isDir);
117
+ const ignored = S.git.isIgnored(below);
118
+ const open = !isDir && ed.open.has(path);
119
+ const dirty = !isDir && ed.dirty.has(path);
120
+ const cut = !!S.clipboard && S.clipboard.mode === 'cut' && S.clipboard.paths.some((held) => isWithin(held, path));
121
+ const words = [];
122
+ if (status) words.push(t(`fileExplorer.git.${status}`)); else if (tone) words.push(t('fileExplorer.git.contains'));
123
+ if (ignored) words.push(t('fileExplorer.git.ignored'));
124
+ if (dirty) words.push(t('fileExplorer.row.unsaved')); else if (open) words.push(t('fileExplorer.row.open'));
125
+ if (item.outside) words.push(t('fileExplorer.row.linkOutside')); else if (item.broken) words.push(t('fileExplorer.row.linkBroken')); else if (item.link) words.push(t('fileExplorer.row.link'));
126
+ if (cut) words.push(t('fileExplorer.row.cut'));
127
+ return {
128
+ tone, letter: status ? GIT_MARKS[status].letter : '', ignored, open, dirty, cut, words,
129
+ active: !isDir && ed.active === path,
130
+ missing: !!item.missing,
131
+ faint: !!item.outside || !!item.broken,
132
+ link: !!item.link,
133
+ color: isDir ? host.colors.of(path) : null,
134
+ match: S.view?.kind === 'filter' && item.hit ? matchRange(item.name, S.filter) : null,
135
+ };
136
+ }
137
+
138
+ const tree = createTree({
139
+ el: treeEl,
140
+ mac,
141
+ icons: { folder: host.folderSvg, forFile: host.fileIcon },
142
+ hooks: {
143
+ itemsOf, isExpanded, rowInfo,
144
+ words: {
145
+ loading: t('fileExplorer.state.loading'),
146
+ retry: t('fileExplorer.state.retry'),
147
+ showMore: t('fileExplorer.state.showMore'),
148
+ emptyFolder: t('fileExplorer.state.emptyFolder'),
149
+ more: (count) => tp('fileExplorer.state.more', count),
150
+ truncated: (shownCount, total) => t('fileExplorer.state.truncated', { shown: shownCount, total }),
151
+ },
152
+ onToggle: toggleFolder,
153
+ onOpen: openEntry,
154
+ onMenu: openMenu,
155
+ onAction,
156
+ onSelection: () => { updateCount(); persistSoon(); },
157
+ onScroll: () => { if (!S.view) persistSoon(); },
158
+ onRetry: (dir) => { S.dirs.delete(dir); tree.renderDir(dir); loadDir(dir).then(() => tree.renderDir(dir)); },
159
+ // `why`: a name that was on its way when its row went, and was not kept: the server's reason.
160
+ onEditLost: (name, why = '') => { if (why || name) notice.show(why || t('fileExplorer.edit.gone', { name }), { tone: 'error' }); },
161
+ acceptsFiles: () => api.abilities().ops,
162
+ onDrop: ({ rows, transfer, into, copy }) => (rows ? ops.transfer(rows, into, { copy }) : ops.upload(transfer, into)),
163
+ onDragStart: (paths) => sender.dragStart(paths, saySent(paths)),
164
+ onDragEnd: () => sender.dragEnd(),
165
+ },
166
+ });
167
+
168
+ const editor = createEditorLink({
169
+ openFile: host.editor.openFile,
170
+ save: host.editor.save,
171
+ tabs: getEditorTabs,
172
+ repoint: repointEditorTab,
173
+ subscribe: (listener) => on('editor-tabs-changed', listener),
174
+ onChange: () => { tree.refreshRows(); syncToolbar(); },
175
+ });
176
+ const sender = createSender({ deps: host.send, t });
177
+ const ops = createOps({
178
+ api, t, tp, tree, notice, editor,
179
+ openFile: host.editor.openFile,
180
+ root: () => S.root,
181
+ itemsIn: (dir) => (S.dirs.get(dir)?.state === 'ready' ? S.dirs.get(dir).items : null),
182
+ openFolder,
183
+ refresh: refreshDirs,
184
+ moved,
185
+ clipboard: { get: () => S.clipboard, set: (value) => { S.clipboard = value; tree.refreshRows(); } },
186
+ });
187
+
188
+ // ── Reading folders ──
189
+
190
+ /**
191
+ * Read one folder. Answers whether what it shows changed. Of two readings of a folder that overlap (it is being
192
+ * opened, and the watcher tells of a change in it) the later one counts: the older answer may arrive last, and
193
+ * would put the folder back as it was. It is dropped.
194
+ */
195
+ async function loadDir(dir) {
196
+ const mine = epoch;
197
+ const latest = reads.take(dir);
198
+ const before = S.dirs.get(dir);
199
+ if (!before) S.dirs.set(dir, { state: 'loading' });
200
+ const got = await api.list(dir);
201
+ if (mine !== epoch || !latest()) return false;
202
+ if (!got.ok) {
203
+ // A folder that was read stays as it is while the server does not answer: it is read again later.
204
+ if (before?.state === 'ready' && got.code === 'offline') return false;
205
+ S.dirs.set(dir, { state: 'error', failure: got });
206
+ return true;
207
+ }
208
+ const next = { state: 'ready', items: got.items, total: got.total ?? got.items.length, truncated: !!got.truncated };
209
+ S.dirs.set(dir, next);
210
+ if (before?.state !== 'ready') return true;
211
+ return !(before.truncated === next.truncated && sameListing(sortItems(before.items), sortItems(next.items)));
212
+ }
213
+
214
+ // The open folders that are in sight: every folder on the way to them is open too, and is still there.
215
+ function shownDirs() {
216
+ const out = [S.root];
217
+ const open = S.view ? S.viewOpen : S.expanded;
218
+ const walk = (dir) => {
219
+ const known = S.dirs.get(dir);
220
+ if (known?.state !== 'ready') return;
221
+ for (const item of known.items) {
222
+ if (item.type !== 'dir') continue;
223
+ const path = `${dir}/${item.name}`;
224
+ if (open.has(path)) { out.push(path); walk(path); }
225
+ }
226
+ };
227
+ if (S.root) walk(S.root);
228
+ return out;
229
+ }
230
+
231
+ // Open folders that are gone from disk are forgotten, with what was read of them.
232
+ function prune() {
233
+ if (!S.root) return;
234
+ const there = (path) => {
235
+ for (let at = path; at !== S.root;) {
236
+ const parent = dirName(at);
237
+ const known = S.dirs.get(parent);
238
+ if (known?.state === 'ready' && !known.items.some((item) => item.type === 'dir' && item.name === baseName(at))) return false;
239
+ if (!isWithin(S.root, parent) || parent === at) return false;
240
+ at = parent;
241
+ }
242
+ return true;
243
+ };
244
+ for (const path of [...S.expanded]) if (!isWithin(S.root, path) || !there(path)) S.expanded.delete(path);
245
+ for (const path of [...S.dirs.keys()]) if (path !== S.root && !there(path)) S.dirs.delete(path);
246
+ }
247
+
248
+ /** Read folders again (those that were read before) and draw the ones that changed. */
249
+ async function refreshDirs(dirs) {
250
+ const mine = epoch;
251
+ const wanted = [...new Set(dirs)].filter((dir) => S.dirs.has(dir) || dir === S.root);
252
+ const changed = await Promise.all(wanted.map((dir) => loadDir(dir)));
253
+ if (mine !== epoch) return;
254
+ prune();
255
+ wanted.forEach((dir, at) => { if (changed[at]) tree.renderDir(dir); });
256
+ if (changed.some(Boolean)) { syncWatch(); updateCount(); }
257
+ queueGit();
258
+ }
259
+ const refreshAll = () => refreshDirs(shownDirs());
260
+
261
+ /** Open a folder in the tree (and those on the way to it) and wait for its rows. */
262
+ async function openFolder(dir) {
263
+ if (!S.root || !isWithin(S.root, dir)) return false;
264
+ const mine = epoch;
265
+ const chain = [];
266
+ for (let at = dir; at !== S.root; at = dirName(at)) chain.unshift(at);
267
+ for (const path of [S.root, ...chain]) {
268
+ if (path !== S.root) { (S.view ? S.viewOpen : S.expanded).add(path); S.viewClosed.delete(path); }
269
+ if (S.dirs.get(path)?.state !== 'ready' && !(S.view && S.view.dirs.has(rel(path)))) await loadDir(path);
270
+ if (mine !== epoch) return false;
271
+ }
272
+ tree.render();
273
+ syncWatch();
274
+ persistSoon();
275
+ return dir === S.root || tree.has(dir);
276
+ }
277
+
278
+ function toggleFolder(path, open) {
279
+ const set = S.view ? S.viewOpen : S.expanded;
280
+ if (open) {
281
+ set.add(path);
282
+ S.viewClosed.delete(path);
283
+ const virtual = S.view && S.view.dirs.has(rel(path));
284
+ // A folder that was read before shows what it held at once and is read again behind it: nothing watched it
285
+ // while it was closed.
286
+ if (!virtual && S.dirs.get(path)?.state === 'ready') refreshDirs([path]);
287
+ else if (!virtual) loadDir(path).then(() => { tree.renderDir(path); syncWatch(); });
288
+ } else {
289
+ set.delete(path);
290
+ if (S.view) S.viewClosed.add(path);
291
+ }
292
+ tree.renderDir(path);
293
+ syncWatch();
294
+ persistSoon();
295
+ }
296
+
297
+ // ── Git ──
298
+
299
+ async function loadGit() {
300
+ if (!S.root) return;
301
+ if (gitBusy) { gitAgain = true; return; }
302
+ gitBusy = true;
303
+ const mine = epoch;
304
+ const got = await api.git(S.root);
305
+ gitBusy = false;
306
+ if (mine === epoch && got.ok) {
307
+ S.git = gitIndex(got);
308
+ host.onGit({ isGit: S.git.isGit, branch: S.git.branch, count: S.git.count });
309
+ if (S.changedOnly) { buildView(); tree.render(); } else tree.refreshRows();
310
+ updateCount();
311
+ }
312
+ if (gitAgain) { gitAgain = false; queueGit(); }
313
+ }
314
+ // Soon after the last sign of a change, and never more often than once in a while: a file that is written
315
+ // without pause (a log, a database) does not make git run without pause.
316
+ function queueGit() {
317
+ clearTimeout(gitTimer);
318
+ gitTimer = setTimeout(() => { lastGitAt = Date.now(); loadGit(); }, Math.max(GIT_DELAY_MS, lastGitAt + GIT_MIN_INTERVAL_MS - Date.now()));
319
+ }
320
+
321
+ // ── The filter and "changed only" ──
322
+
323
+ function buildView() {
324
+ const query = S.filter;
325
+ if (S.changedOnly) {
326
+ const all = S.git.changed();
327
+ const entries = query ? all.filter((entry) => nameMatches(query.includes('/') ? entry.path : baseName(entry.path), query)) : all;
328
+ S.view = {
329
+ kind: 'changed', dirs: buildPathTree(entries), count: entries.length,
330
+ note: !S.git.isGit ? t('fileExplorer.state.notRepo') : entries.length ? '' : t(query ? 'fileExplorer.state.noMatch' : 'fileExplorer.state.noChanges', { query }),
331
+ };
332
+ } else if (query) {
333
+ const found = S.search?.query === query ? S.search : null;
334
+ const items = found?.items || [];
335
+ S.view = {
336
+ kind: 'filter', dirs: buildPathTree(items), count: items.length, pending: !found || found.pending,
337
+ note: !found || found.pending ? t('fileExplorer.state.searching') : found.failed ? t('fileExplorer.state.searchFailed') : items.length ? '' : t('fileExplorer.state.noMatch', { query }),
338
+ };
339
+ } else {
340
+ S.view = null;
341
+ S.viewOpen = new Set(); S.viewClosed = new Set();
342
+ return;
343
+ }
344
+ // Every folder on the way to a row is open, unless the user closed it in this view; folders they opened stay.
345
+ const onTheWay = [...S.view.dirs.keys()].filter(Boolean).map((below) => `${S.root}/${below}`);
346
+ S.viewOpen = new Set([...onTheWay, ...[...S.viewOpen].filter((path) => !S.view.dirs.has(rel(path)))].filter((path) => !S.viewClosed.has(path)));
347
+ }
348
+
349
+ async function runFilter() {
350
+ const query = S.filter;
351
+ searchAbort?.abort();
352
+ searchAbort = null;
353
+ if (!query || S.changedOnly) { S.search = null; showView(); return; }
354
+ const mine = epoch;
355
+ const control = new AbortController();
356
+ searchAbort = control;
357
+ S.search = { query, pending: true };
358
+ showView();
359
+ const got = await api.search(S.root, query, control.signal);
360
+ if (control.signal.aborted || mine !== epoch || S.filter !== query) return;
361
+ S.search = got.ok ? { query, items: got.items, truncated: got.truncated, partial: got.partial } : { query, items: [], failed: true };
362
+ showView();
363
+ if (got.ok && (got.truncated || got.partial)) notice.show(t(got.truncated ? 'fileExplorer.state.moreMatches' : 'fileExplorer.state.partial', { count: got.items.length }));
364
+ }
365
+
366
+ // Draw the view (or the real tree again, where it was).
367
+ function showView() {
368
+ const had = !!S.view;
369
+ if (!had) realTop = tree.scrollTop();
370
+ buildView();
371
+ tree.render();
372
+ if (!S.view && had) tree.setScrollTop(realTop);
373
+ updateCount();
374
+ syncWatch();
375
+ }
376
+
377
+ function setFilter(text) {
378
+ S.filter = text.trim();
379
+ $('fe-filter-clear').hidden = !S.filter;
380
+ clearTimeout(filterTimer);
381
+ filterTimer = setTimeout(runFilter, S.filter ? FILTER_DELAY_MS : 0);
382
+ }
383
+ function clearFilter() {
384
+ if (filterInput.value) filterInput.value = '';
385
+ if (S.filter) setFilter('');
386
+ }
387
+ function setChangedOnly(on) {
388
+ S.changedOnly = !!on;
389
+ S.viewClosed = new Set();
390
+ syncToolbar();
391
+ searchAbort?.abort();
392
+ if (!S.changedOnly && S.filter) runFilter(); else showView();
393
+ persistSoon();
394
+ }
395
+
396
+ // ── What is remembered ──
397
+
398
+ const readStored = () => { try { return JSON.parse(storage.getItem(treeKey) || 'null'); } catch { return null; } };
399
+ function persist() {
400
+ clearTimeout(persistTimer);
401
+ persistTimer = 0;
402
+ if (!S.root) return;
403
+ const now = tree.selection();
404
+ const state = { expanded: S.expanded, selected: S.view ? [] : now.selected, focus: S.view ? null : now.focus, top: S.view ? realTop : tree.scrollTop(), changedOnly: S.changedOnly };
405
+ try { storage.setItem(treeKey, JSON.stringify(rememberTreeState(readStored(), S.root, state))); } catch { /* storage is not there: nothing is kept */ }
406
+ }
407
+ function persistSoon() { if (!persistTimer) persistTimer = setTimeout(persist, PERSIST_DELAY_MS); }
408
+
409
+ // Entries have another path now: what is open, what was read and what has a colour follow them.
410
+ function moved(pairs) {
411
+ for (const { from, to } of pairs) {
412
+ const swap = (path) => (isWithin(from, path) ? trimSlash(to) + path.slice(trimSlash(from).length) : path);
413
+ S.expanded = new Set([...S.expanded].map(swap));
414
+ for (const path of [...S.dirs.keys()]) if (path !== S.root && isWithin(from, path)) S.dirs.delete(path);
415
+ if (S.clipboard) S.clipboard = { ...S.clipboard, paths: S.clipboard.paths.map(swap) };
416
+ host.colors.move(from, to);
417
+ }
418
+ persistSoon();
419
+ }
420
+
421
+ // ── The disk ──
422
+
423
+ function syncWatch() { watcher?.setDirs(shownDirs()); }
424
+ // Anything may have changed without a word (the watch was lost, is back, or the stream was down): what is shown
425
+ // is read again. At once the first time, and then not more often than a look at the window reads it: a watch
426
+ // that keeps failing reports each time, and the tree is not listed each time.
427
+ function rereadSoon() {
428
+ if (rereadTimer) return;
429
+ rereadTimer = setTimeout(() => { rereadTimer = 0; lastFocusRefresh = Date.now(); refreshAll(); }, Math.max(0, lastFocusRefresh + FOCUS_REFRESH_MS - Date.now()));
430
+ }
431
+ function startWatch() {
432
+ stopWatch();
433
+ if (!S.root || !S.visible || document.visibilityState === 'hidden' || !api.abilities().watch) return;
434
+ lastLive = 'full';
435
+ watcher = api.watch(S.root, {
436
+ // The first change of a quiet spell is read at once; a folder that changes without pause (a database's
437
+ // own files) is read again once a second at most.
438
+ onChange: ({ dirs = [], git }) => {
439
+ if (!dirs.length) { if (git) queueGit(); return; }
440
+ for (const dir of dirs) diskDue.add(trimSlash(dir));
441
+ if (diskTimer) return;
442
+ diskTimer = setTimeout(() => {
443
+ diskTimer = 0;
444
+ lastDiskAt = Date.now();
445
+ const due = [...diskDue];
446
+ diskDue.clear();
447
+ refreshDirs(due);
448
+ }, Math.max(0, lastDiskAt + DISK_MIN_INTERVAL_MS - Date.now()));
449
+ },
450
+ // Up again after it was down: anything may have changed meanwhile.
451
+ onReady: (again) => { syncWatch(); if (again) rereadSoon(); },
452
+ // The server lost its watch on the project. It tries for it again by itself: nothing is sent back.
453
+ onResync: rereadSoon,
454
+ // A stream is not a watch that works: the system may have no watches left for the project, or for some of
455
+ // its folders. Said once per project; from then on the tree is read again when it is looked at and after
456
+ // what is done here, which it is in any case. Watched again after it was not: what happened meanwhile was
457
+ // not told.
458
+ onState: ({ live }) => {
459
+ if (live === 'full' && lastLive !== 'full') rereadSoon();
460
+ lastLive = live;
461
+ if (live === 'full' || saidLimited) return;
462
+ saidLimited = true;
463
+ notice.show(t(live === 'none' ? 'fileExplorer.watch.none' : 'fileExplorer.watch.partial'), { ms: 12000 });
464
+ },
465
+ });
466
+ syncWatch();
467
+ }
468
+ function stopWatch() { watcher?.close(); watcher = null; clearTimeout(diskTimer); diskTimer = 0; diskDue.clear(); clearTimeout(rereadTimer); rereadTimer = 0; }
469
+ // The window is looked at again: read what is shown, whether or not a stream is up. A stream may have no watch
470
+ // behind it; where only the shown folders are watched (Linux) a change inside a closed one never reached the git
471
+ // marks; and an older server has no watch at all. Not more often than once in a while, except after the stream
472
+ // was let go (the tab was hidden): then anything may have changed.
473
+ function onAttention() {
474
+ if (!S.root || !S.visible || document.visibilityState === 'hidden') return;
475
+ const wasDown = api.abilities().watch && !watcher;
476
+ if (wasDown) startWatch();
477
+ if (!wasDown && Date.now() - lastFocusRefresh < FOCUS_REFRESH_MS) return;
478
+ lastFocusRefresh = Date.now();
479
+ refreshAll();
480
+ }
481
+ document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') stopWatch(); else onAttention(); });
482
+ window.addEventListener('focus', onAttention);
483
+ // The page is going: what is remembered is written, and sent on by a request of its own (storage.js has
484
+ // flushed by then: a window's listeners run in the order they were added), and the watch is let go.
485
+ window.addEventListener('pagehide', () => {
486
+ const due = !!persistTimer;
487
+ persist();
488
+ if (due) {
489
+ try {
490
+ const value = storage.getItem(treeKey);
491
+ if (value) navigator.sendBeacon?.('/api/ui-state', new Blob([JSON.stringify({ [treeKey]: JSON.parse(value) })], { type: 'application/json' }));
492
+ } catch { /* the debounced write is what is left */ }
493
+ }
494
+ stopWatch();
495
+ });
496
+
497
+ // ── Opening ──
498
+
499
+ // The tree keeps the keyboard after a file was only looked at (Space): the editor takes it when it has loaded.
500
+ function keepKeyboard(path) {
501
+ const end = Date.now() + 2500;
502
+ const tick = () => {
503
+ if (document.activeElement?.closest?.('#fe-editor-panel')) { tree.focus(); return; }
504
+ if (Date.now() < end && editor.read().active !== path) setTimeout(tick, 60);
505
+ else if (Date.now() < end) setTimeout(() => { if (document.activeElement?.closest?.('#fe-editor-panel')) tree.focus(); }, 120);
506
+ };
507
+ setTimeout(tick, 60);
508
+ }
509
+
510
+ function openEntry(path, item, { pin = false, keep = false } = {}) {
511
+ if (!item) return;
512
+ const name = item.name;
513
+ if (item.missing) { notice.show(t('fileExplorer.open.deleted', { name })); return; }
514
+ if (item.outside) { notice.show(t('fileExplorer.open.linkOutside', { name })); return; }
515
+ if (item.broken) { notice.show(t('fileExplorer.open.linkBroken', { name })); return; }
516
+ const kind = fileKind(name);
517
+ if (kind.kind === 'text') {
518
+ closeFlyout({ refocus: false });
519
+ host.editor.openFile(path, { pin });
520
+ if (keep) keepKeyboard(path);
521
+ return;
522
+ }
523
+ const can = api.abilities();
524
+ const image = kind.kind === 'image';
525
+ const url = image ? api.rawUrl(path) : null;
526
+ const what = [image ? t('fileExplorer.preview.image') : t(`fileExplorer.kind.${kind.type}`), item.size !== undefined ? formatBytes(item.size) : ''].filter(Boolean).join(' · ');
527
+ const why = image ? (url ? '' : t('fileExplorer.preview.noPreview')) : t('fileExplorer.preview.notText');
528
+ openPreviewCard({
529
+ panel,
530
+ row: tree.rowOf(path),
531
+ label: t('fileExplorer.preview.label', { name }),
532
+ name,
533
+ detail: why ? `${what}\n${why}` : what,
534
+ imageUrl: url,
535
+ imageAlt: name,
536
+ imageFailed: t('fileExplorer.preview.failed'),
537
+ actions: [
538
+ ...(kind.alsoText ? [{ label: t('fileExplorer.preview.openAsText'), run: () => host.editor.openFile(path, { pin: true }) }] : []),
539
+ ...(can.reveal ? [{ label: t(`fileExplorer.menu.reveal.${revealWord()}`), run: () => ops.reveal(path) }] : []),
540
+ { label: t('fileExplorer.menu.copyPath.one'), run: () => ops.copyPath([path]) },
541
+ { label: tp('fileExplorer.menu.send', 1, { name: sender.nameOf(sender.current()) }), run: () => sendPaths([path]) },
542
+ ],
543
+ });
544
+ }
545
+
546
+ const saySent = (paths) => (done) => notice.show(
547
+ done.ok ? tp('fileExplorer.send.done', done.count, { name: baseName(paths[0]), target: done.name }) : t('fileExplorer.send.failed', { target: done.name }),
548
+ { tone: done.ok ? 'ok' : 'error' },
549
+ );
550
+ const sendPaths = (paths, target = null) => sender.send(paths, target).then(saySent(paths));
551
+
552
+ /** The editor's go-to-file, for the selected project (the editor's own module, opened beside the panel). */
553
+ function jump() {
554
+ if (!S.root) return;
555
+ let layer = $('fe-jump-layer');
556
+ if (!layer) { layer = document.createElement('div'); layer.id = 'fe-jump-layer'; layer.className = 'fe-jump-layer'; document.body.appendChild(layer); }
557
+ palette ||= createPalette({
558
+ t,
559
+ openMenu: (menu) => openEditorMenu({ host: layer, ...menu }),
560
+ position: () => {
561
+ const frame = panel.getBoundingClientRect();
562
+ const width = Math.min(460, window.innerWidth - 16);
563
+ const free = window.innerWidth - frame.right;
564
+ return { x: free >= width + 16 ? frame.right + (free - width) / 2 : (window.innerWidth - width) / 2, y: (parseFloat(getComputedStyle(panel).paddingTop) || 0) + 12 };
565
+ },
566
+ mac,
567
+ commands: [],
568
+ features: () => [],
569
+ run: () => {},
570
+ project: () => S.root,
571
+ tabs: () => { const ed = editor.state(); return [...ed.open].filter((path) => path !== ed.active).map((path) => ({ path, name: baseName(path) })); },
572
+ openFile: (path) => host.editor.openFile(path, { pin: true }),
573
+ });
574
+ closeFlyout({ refocus: false });
575
+ palette.openFiles();
576
+ }
577
+
578
+ // ── The menu ──
579
+
580
+ const folderFor = (path, item) => (!path ? S.root : item?.type === 'dir' ? path : dirName(path));
581
+
582
+ function menuItems(model, { count }) {
583
+ const target = sender.nameOf(sender.current());
584
+ return model.map((entry) => {
585
+ if (entry.separator) return entry;
586
+ const { id } = entry;
587
+ let label;
588
+ if (id === 'send') label = tp('fileExplorer.menu.send', count, { name: target });
589
+ else if (id === 'delete') label = count > 1 ? t('fileExplorer.menu.deleteMany', { count }) : t('fileExplorer.menu.delete');
590
+ else if (id === 'copyPath' || id === 'copyRelativePath') label = tp(`fileExplorer.menu.${id}`, count);
591
+ else if (id === 'reveal') label = t(`fileExplorer.menu.reveal.${revealWord()}`);
592
+ else label = t(`fileExplorer.menu.${id}`);
593
+ const key = id === 'delete' && mac ? 'Mod-Backspace' : entry.key;
594
+ const why = entry.why === 'restart' ? t('fileExplorer.needsRestart') : entry.why === 'nothingCopied' ? t('fileExplorer.menu.nothingCopied') : entry.why === 'noReveal' ? t('fileExplorer.error.noReveal') : '';
595
+ return { id, label, danger: entry.danger, disabled: entry.disabled, title: why, more: entry.more, checked: entry.checked, keyLabel: key ? formatKey(key, mac) : '' };
596
+ });
597
+ }
598
+ const back = () => ({ id: 'back', back: true, label: t('fileExplorer.menu.back') });
599
+
600
+ function openMenu({ paths = [], row = null, item = null, expanded = false, keyboard = false, y = null }) {
601
+ if (!S.root) return;
602
+ const can = api.abilities();
603
+ const target = paths.length === 0 ? 'empty' : paths.length > 1 ? 'multi' : item?.type === 'dir' ? 'dir' : 'file';
604
+ const model = menuModel({
605
+ target, count: paths.length, caps: { ops: can.ops, reveal: can.reveal }, expanded, canPaste: !!S.clipboard,
606
+ kind: item ? fileKind(item.name).kind : 'text', missing: !!item?.missing, changedOnly: S.changedOnly, sort: S.sort,
607
+ });
608
+ const path = paths[0] || null;
609
+ openContextMenu({
610
+ panel, row, keyboard,
611
+ y: y ?? treeEl.getBoundingClientRect().top + 40,
612
+ label: target === 'empty' ? t('fileExplorer.menu.labelProject') : target === 'multi' ? t('fileExplorer.menu.labelMany', { count: paths.length }) : t('fileExplorer.menu.label', { name: item?.name || '' }),
613
+ items: menuItems(model, { count: Math.max(1, paths.length) }),
614
+ onSelect: (chosen) => act(chosen.id, { paths, path, item }),
615
+ });
616
+ }
617
+
618
+ // What a menu item does. A list it returns is the menu's next page.
619
+ function act(id, { paths, path, item }) {
620
+ const all = paths.length ? paths : [S.root];
621
+ if (id.startsWith('to:')) { sendPaths(all, id.slice(3)); return undefined; }
622
+ if (id.startsWith('color:')) { host.colors.set(path, id.slice(6) || null); tree.refreshRows(); return undefined; }
623
+ if (id.startsWith('sort:')) { S.sort = id.slice(5); try { storage.setItem(sortKey, S.sort); } catch { /* not kept */ } tree.render(); return undefined; }
624
+ switch (id) {
625
+ case 'open': openEntry(path, item, { pin: false }); break;
626
+ case 'openKeep': openEntry(path, item, { pin: true }); break;
627
+ case 'preview': openEntry(path, item); break;
628
+ case 'openAsText': host.editor.openFile(path, { pin: true }); break;
629
+ case 'send': sendPaths(all); break;
630
+ case 'sendTo': return [back(), ...sender.targets().map((entry) => ({ id: `to:${entry.id}`, label: entry.name, icon: 'send' }))];
631
+ case 'newFile': ops.create('file', folderFor(path, item)); break;
632
+ case 'newFolder': ops.create('dir', folderFor(path, item)); break;
633
+ case 'expand': toggleFolder(path, true); break;
634
+ case 'collapse': toggleFolder(path, false); break;
635
+ case 'collapseChildren':
636
+ for (const open of [...(S.view ? S.viewOpen : S.expanded)]) if (open !== path && isWithin(path, open)) { (S.view ? S.viewOpen : S.expanded).delete(open); if (S.view) S.viewClosed.add(open); }
637
+ tree.renderDir(path); syncWatch(); persistSoon();
638
+ break;
639
+ case 'cut': ops.cut(all); break;
640
+ case 'copy': ops.copy(all); break;
641
+ case 'paste': ops.paste(folderFor(path, item)); break;
642
+ case 'duplicate': ops.duplicate(all); break;
643
+ case 'copyPath': ops.copyPath(all); break;
644
+ case 'copyRelativePath': ops.copyPath(all, { relative: true }); break;
645
+ case 'copyName': ops.copyName(path); break;
646
+ case 'color': {
647
+ const now = host.colors.of(path) || host.colors.all[0].hex;
648
+ return [back(), ...host.colors.all.map((color) => ({ id: `color:${color.hex === host.colors.all[0].hex ? '' : color.hex}`, swatch: color.hex, label: t(`fileExplorer.colors.${color.key}`), checked: color.hex === now }))];
649
+ }
650
+ case 'rename': ops.rename(path); break;
651
+ case 'delete': ops.remove(all); break;
652
+ case 'reveal': ops.reveal(path || S.root); break;
653
+ case 'changedOnly': setChangedOnly(!S.changedOnly); break;
654
+ case 'sortBy': return [back(), ...SORTS.map((mode) => ({ id: `sort:${mode}`, label: t(`fileExplorer.menu.sort.${mode}`), checked: S.sort === mode }))];
655
+ case 'expandAll': expandLevel(); break;
656
+ case 'collapseAll': collapseAll(); break;
657
+ case 'refresh': refreshAll(); break;
658
+ default:
659
+ }
660
+ return undefined;
661
+ }
662
+
663
+ // A key of the tree that is about files rather than rows.
664
+ function onAction(action, { paths, focus }) {
665
+ const item = focus ? tree.itemOf(focus) : null;
666
+ switch (action) {
667
+ case 'delete': if (paths.length) ops.remove(paths); break;
668
+ case 'rename': if (focus) ops.rename(focus); break;
669
+ case 'copy': ops.copy(paths); break;
670
+ case 'cut': ops.cut(paths); break;
671
+ case 'paste': ops.paste(folderFor(focus, item)); break;
672
+ case 'copyPath': ops.copyPath(paths.length ? paths : [S.root]); break;
673
+ case 'copyRelativePath': ops.copyPath(paths.length ? paths : [S.root], { relative: true }); break;
674
+ case 'jump': jump(); break;
675
+ case 'escape':
676
+ if (isFlyoutOpen()) closeFlyout();
677
+ else if (S.clipboard) { S.clipboard = null; tree.refreshRows(); notice.clear(); }
678
+ else if (S.filter) clearFilter();
679
+ else if (focus) tree.select([focus], { focus });
680
+ break;
681
+ default:
682
+ }
683
+ }
684
+
685
+ function collapseAll() {
686
+ if (S.view) { for (const path of S.viewOpen) S.viewClosed.add(path); S.viewOpen = new Set(); } else S.expanded.clear();
687
+ tree.render();
688
+ syncWatch();
689
+ persistSoon();
690
+ }
691
+ // Open every closed folder that is in sight, one level.
692
+ async function expandLevel() {
693
+ const closed = tree.order().filter((path) => tree.itemOf(path)?.type === 'dir' && !isExpanded(path)).slice(0, EXPAND_LEVEL_MAX);
694
+ const set = S.view ? S.viewOpen : S.expanded;
695
+ for (const path of closed) { set.add(path); S.viewClosed.delete(path); }
696
+ tree.render();
697
+ await Promise.all(closed.filter((path) => S.dirs.get(path)?.state !== 'ready' && !(S.view && S.view.dirs.has(rel(path)))).map(loadDir));
698
+ tree.render();
699
+ syncWatch();
700
+ persistSoon();
701
+ }
702
+
703
+ // ── The panel around the tree ──
704
+
705
+ function updateCount() {
706
+ const count = $('fe-count');
707
+ if (!count) return;
708
+ const picked = tree.selection().selected.length;
709
+ let text = '';
710
+ if (!S.root) text = '';
711
+ else if (picked > 1) text = tp('fileExplorer.count.selected', picked);
712
+ else if (S.view?.pending) text = t('fileExplorer.state.searching');
713
+ else if (S.view) text = tp(S.view.kind === 'changed' ? 'fileExplorer.count.changed' : 'fileExplorer.count.matches', S.view.count);
714
+ else { const known = S.dirs.get(S.root); text = known?.state === 'ready' ? tp('fileExplorer.count.items', known.total) : ''; }
715
+ count.textContent = text;
716
+ const hint = $('fe-restart-hint');
717
+ if (hint) hint.hidden = !(api.abilities().known && !api.abilities().ops);
718
+ }
719
+ function syncToolbar() {
720
+ const changed = $('fe-changed-only');
721
+ if (changed) { changed.setAttribute('aria-pressed', String(S.changedOnly)); changed.classList.toggle('active', S.changedOnly); }
722
+ const shown = $('fe-reveal-active');
723
+ const active = editor.state().active;
724
+ if (shown) shown.disabled = !(active && S.root && isWithin(S.root, active));
725
+ }
726
+
727
+ function wire() {
728
+ const on = (id, handler) => $(id)?.addEventListener('click', handler);
729
+ const focusedFolder = () => { const at = tree.selection().focus; return folderFor(at, at ? tree.itemOf(at) : null); };
730
+ on('fe-new-file', () => ops.create('file', focusedFolder()));
731
+ on('fe-new-folder', () => ops.create('dir', focusedFolder()));
732
+ on('fe-goto-file', jump);
733
+ on('fe-changed-only', () => setChangedOnly(!S.changedOnly));
734
+ on('fe-reveal-active', async () => {
735
+ const active = editor.state().active;
736
+ if (!active) { notice.show(t('fileExplorer.reveal.none')); return; }
737
+ if (!(await reveal(active))) notice.show(t('fileExplorer.reveal.notShown', { name: baseName(active) }));
738
+ else tree.focus();
739
+ });
740
+ on('fe-collapse-all', collapseAll);
741
+ // Refresh also asks what the server has: one that was restarted since the page loaded is found out here.
742
+ on('fe-refresh', async () => { await host.readServer?.(); await api.capabilities(); syncToolbar(); await refreshAll(); if (!watcher) startWatch(); });
743
+ on('fe-more', (event) => { const box = event.currentTarget.getBoundingClientRect(); openMenu({ y: box.top + box.height / 2, keyboard: event.detail === 0 }); });
744
+ on('fe-filter-clear', () => { clearFilter(); filterInput.focus(); });
745
+ filterInput.addEventListener('input', () => setFilter(filterInput.value));
746
+ filterInput.addEventListener('keydown', (event) => {
747
+ if (event.key === 'Escape' && filterInput.value) { event.preventDefault(); event.stopPropagation(); clearFilter(); }
748
+ else if (event.key === 'ArrowDown' || event.key === 'Enter') { event.preventDefault(); tree.focus(); }
749
+ });
750
+ // Below the rows: the project itself.
751
+ treeEl.addEventListener('pointerdown', (event) => { if (event.target === treeEl && event.button === 0 && !tree.isEditing()) tree.select([]); });
752
+ }
753
+
754
+ // ── A project ──
755
+
756
+ function showNoProject() {
757
+ tree.setRoot(null);
758
+ const block = document.createElement('div');
759
+ block.className = 'fe-empty';
760
+ const words = document.createElement('p');
761
+ words.textContent = t('fileExplorer.state.noProject');
762
+ const button = document.createElement('button');
763
+ button.type = 'button';
764
+ button.className = 'fe-modal-btn fe-modal-cancel';
765
+ button.textContent = t('fileExplorer.state.addProject');
766
+ button.addEventListener('click', () => host.addProject());
767
+ block.append(words, button);
768
+ treeEl.append(block);
769
+ updateCount();
770
+ }
771
+
772
+ /** Show a project (or none): its tree comes back as it was left. */
773
+ function setProject(project) {
774
+ persist();
775
+ epoch++;
776
+ stopWatch();
777
+ saidLimited = false;
778
+ closeFlyout({ refocus: false });
779
+ searchAbort?.abort();
780
+ clearTimeout(gitTimer);
781
+ S.dirs = new Map(); S.git = gitIndex(null); S.view = null; S.viewOpen = new Set(); S.viewClosed = new Set();
782
+ S.search = null; S.filter = ''; S.clipboard = null;
783
+ reads.clear();
784
+ if (filterInput) { filterInput.value = ''; $('fe-filter-clear').hidden = true; }
785
+ host.onGit({ isGit: false, branch: '', count: 0 });
786
+ if (!project?.path) { S.root = null; S.label = ''; S.expanded = new Set(); showNoProject(); loaded = Promise.resolve(); return loaded; }
787
+ S.root = trimSlash(project.path);
788
+ S.label = project.label || baseName(S.root);
789
+ treeEl.setAttribute('aria-label', t('fileExplorer.tree', { project: S.label }));
790
+ const kept = recallTreeState(readStored(), S.root);
791
+ S.expanded = new Set(kept.expanded.filter((path) => isWithin(S.root, path) && path !== S.root));
792
+ S.changedOnly = kept.changedOnly;
793
+ syncToolbar();
794
+ tree.setRoot(S.root);
795
+ tree.render();
796
+ const mine = epoch;
797
+ loaded = (async () => {
798
+ await api.capabilities();
799
+ if (mine !== epoch) return;
800
+ await loadDir(S.root);
801
+ // The folders that were open, level by level: one that is gone takes its own with it.
802
+ for (let round = 0; round < 40; round++) {
803
+ if (mine !== epoch) return;
804
+ prune();
805
+ const next = shownDirs().filter((dir) => !S.dirs.has(dir));
806
+ if (!next.length) break;
807
+ await Promise.all(next.map(loadDir));
808
+ }
809
+ if (mine !== epoch) return;
810
+ await (S.changedOnly ? loadGit() : (loadGit(), null));
811
+ if (mine !== epoch) return;
812
+ buildView();
813
+ tree.render();
814
+ if (!S.view) {
815
+ tree.select(kept.selected, { focus: kept.focus, quiet: true, scroll: false });
816
+ tree.setScrollTop(kept.top);
817
+ }
818
+ updateCount();
819
+ startWatch();
820
+ })();
821
+ return loaded;
822
+ }
823
+
824
+ /** The panel is shown or hidden: the disk is followed only while it is shown. */
825
+ function setVisible(visible) {
826
+ S.visible = !!visible;
827
+ if (S.visible) { if (S.root) { startWatch(); if (S.dirs.size) refreshAll(); } } else { persist(); stopWatch(); closeFlyout({ refocus: false }); }
828
+ }
829
+
830
+ /** Show a file of the project in the tree: its folders open, its row is selected and marked. */
831
+ async function reveal(filePath) {
832
+ const target = trimSlash(filePath);
833
+ await loaded;
834
+ if (!S.root || target === S.root || !isWithin(S.root, target)) return false;
835
+ if (S.filter) { filterInput.value = ''; S.filter = ''; $('fe-filter-clear').hidden = true; S.search = null; }
836
+ if (S.changedOnly && !S.git.statusOf(rel(target), false)) { S.changedOnly = false; syncToolbar(); }
837
+ buildView();
838
+ if (!(await openFolder(dirName(target))) || !tree.has(target)) { tree.render(); return false; }
839
+ tree.select([target], { focus: target });
840
+ tree.flash(target);
841
+ updateCount();
842
+ return true;
843
+ }
844
+
845
+ wire();
846
+ editor.start();
847
+ syncToolbar();
848
+
849
+ return {
850
+ setProject,
851
+ setVisible,
852
+ reveal,
853
+ refresh: refreshAll,
854
+ collapseAll,
855
+ /** The icons changed: every row is drawn again. */
856
+ redraw() { if (S.root) { const now = tree.selection(); const top = tree.scrollTop(); tree.setRoot(S.root); tree.render(); tree.select(now.selected, { focus: now.focus, quiet: true, scroll: false }); tree.setScrollTop(top); } },
857
+ whenLoaded: () => loaded,
858
+ /** Close the menu or the preview that is open (the editor is opening). */
859
+ closeMenus: () => closeFlyout({ refocus: false }),
860
+ notice,
861
+ api,
862
+ state: S,
863
+ tree,
864
+ };
865
+ }