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.
- package/CHANGELOG.md +65 -0
- package/NOTICE +5 -0
- package/README.md +1 -1
- package/THIRD-PARTY-LICENSES.md +51 -0
- package/benchmarks/fixtures/browser/README.md +1 -1
- package/benchmarks/fixtures/desktop/README.md +1 -1
- package/docs/whatsapp.md +2 -2
- package/hooks/claude-code/pre-compact.mjs +11 -0
- package/hooks/claude-code/prompt-submit.mjs +60 -184
- package/hooks/claude-code/session-start.mjs +73 -16
- package/hooks/claude-code/shared.mjs +20 -0
- package/hooks/claude-code/stop.mjs +5 -5
- package/launcher.mjs +40 -0
- package/lib/claude-hooks.js +61 -8
- package/lib/client-config-repair.js +4 -3
- package/lib/data-home-migration.js +4 -0
- package/lib/desktop-pwa.js +206 -0
- package/lib/external-tools.js +221 -0
- package/lib/first-launch-protection.js +5 -0
- package/lib/packaged-runtime.js +110 -0
- package/lib/start-launcher.js +1396 -0
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -3
- package/neural-interface/i18n/en.json +1540 -154
- package/neural-interface/i18n/pt-BR.json +1538 -153
- package/neural-interface/lib/assistant-api.js +62 -6
- package/neural-interface/lib/assistant-brains/claude.js +4 -0
- package/neural-interface/lib/assistant-catalog.js +179 -27
- package/neural-interface/lib/assistant-config.js +31 -2
- package/neural-interface/lib/assistant-dispatch.js +62 -13
- package/neural-interface/lib/assistant-envelope.js +9 -6
- package/neural-interface/lib/assistant-hidden-models.js +393 -66
- package/neural-interface/lib/assistant-memory.js +24 -10
- package/neural-interface/lib/assistant-persona.js +6 -2
- package/neural-interface/lib/assistant-pricing.js +21 -6
- package/neural-interface/lib/assistant-runtime.js +181 -12
- package/neural-interface/lib/assistant-task-prompt.js +13 -3
- package/neural-interface/lib/assistant-test-density.js +142 -0
- package/neural-interface/lib/assistant-user-context-budgets.js +123 -0
- package/neural-interface/lib/assistant-user-context.js +908 -0
- package/neural-interface/lib/assistant-user-knowledge.js +328 -0
- package/neural-interface/lib/assistant-user-learn.js +404 -0
- package/neural-interface/lib/assistant-user-threads.js +226 -0
- package/neural-interface/lib/assistant-user-writer.js +181 -0
- package/neural-interface/lib/augmented-path.js +23 -3
- package/neural-interface/lib/browser-background-tab.js +134 -0
- package/neural-interface/lib/browser-language.js +83 -0
- package/neural-interface/lib/browser-screenshot.js +678 -61
- package/neural-interface/lib/claude-agent-bridge.js +87 -153
- package/neural-interface/lib/claude-executable.js +241 -0
- package/neural-interface/lib/claude-model-catalog.js +82 -93
- package/neural-interface/lib/claude-temporary.js +46 -7
- package/neural-interface/lib/cloudflared/install.js +533 -0
- package/neural-interface/lib/cloudflared/manager.js +545 -0
- package/neural-interface/lib/cloudflared/pins.js +115 -0
- package/neural-interface/lib/cloudflared/pins.json +63 -0
- package/neural-interface/lib/cloudflared/tunnel.js +265 -0
- package/neural-interface/lib/editor-completion-endpoint.js +264 -0
- package/neural-interface/lib/editor-completion-opencode.js +436 -0
- package/neural-interface/lib/editor-completion.js +714 -0
- package/neural-interface/lib/editor-config.js +149 -0
- package/neural-interface/lib/editor-files-api.js +268 -0
- package/neural-interface/lib/editor-git-api.js +134 -0
- package/neural-interface/lib/file-explorer-api.js +1039 -0
- package/neural-interface/lib/file-explorer-system.js +355 -0
- package/neural-interface/lib/file-explorer-trash.ps1 +85 -0
- package/neural-interface/lib/file-explorer-watch.js +239 -0
- package/neural-interface/lib/git-diff-api.js +103 -0
- package/neural-interface/lib/git-read.js +202 -0
- package/neural-interface/lib/greeting/api.js +89 -0
- package/neural-interface/lib/greeting/budgets.js +90 -0
- package/neural-interface/lib/greeting/engine.js +461 -0
- package/neural-interface/lib/greeting/facts.js +320 -0
- package/neural-interface/lib/greeting/sources.js +252 -0
- package/neural-interface/lib/greeting/writer.js +139 -0
- package/neural-interface/lib/http-guards.js +189 -13
- package/neural-interface/lib/machine-names.js +88 -0
- package/neural-interface/lib/memory-api.js +31 -7
- package/neural-interface/lib/native-binary-runtime.js +69 -337
- package/neural-interface/lib/native-loop-providers.js +40 -49
- package/neural-interface/lib/offline-page.js +54 -0
- package/neural-interface/lib/plugin-installer.js +4 -2
- package/neural-interface/lib/project-storage.js +5 -7
- package/neural-interface/lib/request-gate.js +589 -0
- package/neural-interface/lib/session-title-generator.js +5 -0
- package/neural-interface/lib/synabun-update-plan.js +19 -0
- package/neural-interface/lib/ui-blob-api.js +108 -0
- package/neural-interface/lib/ui-blob-store.js +68 -0
- package/neural-interface/package-lock.json +701 -253
- package/neural-interface/package.json +13 -2
- package/neural-interface/public/install-app.html +34 -0
- package/neural-interface/public/manifest.json +2 -0
- package/neural-interface/public/offline.html +402 -66
- package/neural-interface/public/onboarding.html +264 -147
- package/neural-interface/public/shared/agent-runtime-options.js +22 -6
- package/neural-interface/public/shared/assistant/asst-automation.js +286 -0
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +312 -62
- package/neural-interface/public/shared/assistant/asst-composer.js +11 -1
- package/neural-interface/public/shared/assistant/asst-mascot.js +26 -64
- package/neural-interface/public/shared/assistant/asst-menu.js +12 -8
- package/neural-interface/public/shared/assistant/asst-panel.js +150 -28
- package/neural-interface/public/shared/assistant/asst-rack-styles.js +0 -3
- package/neural-interface/public/shared/assistant/asst-render.js +219 -67
- package/neural-interface/public/shared/assistant/asst-route-models.js +108 -0
- package/neural-interface/public/shared/assistant/asst-route.js +23 -329
- package/neural-interface/public/shared/assistant/asst-routes-editor.js +495 -0
- package/neural-interface/public/shared/assistant/asst-sanitize.js +8 -1
- package/neural-interface/public/shared/assistant/asst-sidepanel.js +31 -9
- package/neural-interface/public/shared/assistant/asst-state.js +185 -1
- package/neural-interface/public/shared/assistant/asst-styles.js +162 -6
- package/neural-interface/public/shared/blob-store.js +111 -22
- package/neural-interface/public/shared/cdx/cdx-panel.js +32 -11
- package/neural-interface/public/shared/cdx/cdx-render.js +25 -89
- package/neural-interface/public/shared/cdx/cdx-styles.js +36 -0
- package/neural-interface/public/shared/cdx/cdx-tabs.js +87 -4
- package/neural-interface/public/shared/command-runner-model.js +30 -0
- package/neural-interface/public/shared/constants.js +6 -1
- package/neural-interface/public/shared/cp/cp-rehydrate.js +2 -1
- package/neural-interface/public/shared/cp/cp-styles.js +15 -0
- package/neural-interface/public/shared/editor/editor-ai-logic.js +409 -0
- package/neural-interface/public/shared/editor/editor-ai.js +619 -0
- package/neural-interface/public/shared/editor/editor-commands.js +212 -0
- package/neural-interface/public/shared/editor/editor-compare.js +103 -0
- package/neural-interface/public/shared/editor/editor-completion-list.js +172 -0
- package/neural-interface/public/shared/editor/editor-doc.js +257 -0
- package/neural-interface/public/shared/editor/editor-git-logic.js +131 -0
- package/neural-interface/public/shared/editor/editor-git.js +95 -0
- package/neural-interface/public/shared/editor/editor-languages.js +156 -0
- package/neural-interface/public/shared/editor/editor-lint.js +229 -0
- package/neural-interface/public/shared/editor/editor-loader.js +51 -0
- package/neural-interface/public/shared/editor/editor-markdown.js +90 -0
- package/neural-interface/public/shared/editor/editor-menu.js +233 -0
- package/neural-interface/public/shared/editor/editor-minimap.js +371 -0
- package/neural-interface/public/shared/editor/editor-palette-logic.js +192 -0
- package/neural-interface/public/shared/editor/editor-palette.js +125 -0
- package/neural-interface/public/shared/editor/editor-panel-logic.js +288 -0
- package/neural-interface/public/shared/editor/editor-panel.js +2099 -0
- package/neural-interface/public/shared/editor/editor-prefs.js +224 -0
- package/neural-interface/public/shared/editor/editor-safety.js +500 -0
- package/neural-interface/public/shared/editor/editor-search.js +352 -0
- package/neural-interface/public/shared/editor/editor-statusbar.js +144 -0
- package/neural-interface/public/shared/editor/editor-tabs-logic.js +331 -0
- package/neural-interface/public/shared/editor/editor-tabs.js +233 -0
- package/neural-interface/public/shared/editor/editor-theme.js +244 -0
- package/neural-interface/public/shared/editor/editor-view.js +332 -0
- package/neural-interface/public/shared/explorer/fx-api.js +216 -0
- package/neural-interface/public/shared/explorer/fx-links.js +204 -0
- package/neural-interface/public/shared/explorer/fx-logic.js +537 -0
- package/neural-interface/public/shared/explorer/fx-main.js +865 -0
- package/neural-interface/public/shared/explorer/fx-names.js +87 -0
- package/neural-interface/public/shared/explorer/fx-ops.js +370 -0
- package/neural-interface/public/shared/explorer/fx-popups.js +490 -0
- package/neural-interface/public/shared/explorer/fx-tree.js +790 -0
- package/neural-interface/public/shared/file-links.js +339 -0
- package/neural-interface/public/shared/greeting/greeting-card.js +178 -0
- package/neural-interface/public/shared/greeting/greeting-view.js +148 -0
- package/neural-interface/public/shared/html-shell.js +169 -170
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-greeting.js +47 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +99 -17
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +5 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +40 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +3 -2
- package/neural-interface/public/shared/pwa-install-page.js +62 -0
- package/neural-interface/public/shared/pwa-install.css +12 -0
- package/neural-interface/public/shared/pwa-install.js +127 -0
- package/neural-interface/public/shared/settings/settings-code-editor-pane.js +412 -0
- package/neural-interface/public/shared/settings/settings-code-editor.js +328 -0
- package/neural-interface/public/shared/settings/settings-greeting.js +133 -0
- package/neural-interface/public/shared/settings/settings-ia.js +18 -4
- package/neural-interface/public/shared/settings/settings-rules.js +160 -0
- package/neural-interface/public/shared/settings/settings-user-context.js +154 -0
- package/neural-interface/public/shared/start-bridge.js +802 -0
- package/neural-interface/public/shared/styleguide/sg-colors.js +5 -2
- package/neural-interface/public/shared/styleguide/sg-import-export.js +4 -3
- package/neural-interface/public/shared/styleguide/sg-kit.js +4 -1
- package/neural-interface/public/shared/styleguide/sg-panel.js +9 -1
- package/neural-interface/public/shared/styleguide/sg-typography.js +1 -0
- package/neural-interface/public/shared/styles.css +2219 -1117
- package/neural-interface/public/shared/synabun-mascot.js +20 -56
- package/neural-interface/public/shared/term-ready.js +307 -0
- package/neural-interface/public/shared/tunnel-view.js +167 -0
- package/neural-interface/public/shared/ui-claude-panel.js +160 -19
- package/neural-interface/public/shared/ui-command-runner.js +251 -468
- package/neural-interface/public/shared/ui-detail.js +15 -0
- package/neural-interface/public/shared/ui-explorer.js +22 -0
- package/neural-interface/public/shared/ui-file-explorer.js +481 -2496
- package/neural-interface/public/shared/ui-file-links.js +103 -0
- package/neural-interface/public/shared/ui-help.js +26 -0
- package/neural-interface/public/shared/ui-invite.js +126 -116
- package/neural-interface/public/shared/ui-judgments.js +1 -1
- package/neural-interface/public/shared/ui-keybinds.js +4 -0
- package/neural-interface/public/shared/ui-loading.js +367 -12
- package/neural-interface/public/shared/ui-menubar.js +21 -55
- package/neural-interface/public/shared/ui-native-loop-router-state.js +11 -0
- package/neural-interface/public/shared/ui-native-loop-router.js +15 -2
- package/neural-interface/public/shared/ui-navbar.js +45 -21
- package/neural-interface/public/shared/ui-settings.js +491 -547
- package/neural-interface/public/shared/ui-sidepanel-runtime.js +2 -1
- package/neural-interface/public/shared/ui-sync.js +16 -0
- package/neural-interface/public/shared/ui-terminal.js +153 -149
- package/neural-interface/public/shared/ui-tunnel.js +130 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +6 -2
- package/neural-interface/public/shared/ui-whiteboard.js +12 -0
- package/neural-interface/public/shared/ui-worker-end.js +245 -0
- package/neural-interface/public/shared/ui-workspaces.js +41 -27
- package/neural-interface/public/shared/worker-end.js +251 -0
- package/neural-interface/public/sw.js +3 -1
- package/neural-interface/public/variant/2d/main.js +2 -1
- package/neural-interface/public/variant/3d/main.js +2 -1
- package/neural-interface/public/vendor/codemirror/chunks/apl-CSDKT72Z.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/asciiarmor-JYYPU5BM.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/asn1-NRWCZDA6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/asterisk-5OU6D4ZQ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/brainfuck-ID4N4SYC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-4RZWGCS2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-ACTJM3JR.js +11 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-S6BMAXLK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-TO2RDOHQ.js +3 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-US2GMZZQ.js +13 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-WYYO5ZAJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-XALZTDJC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-YRNUTRDV.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-Z3QOKH62.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/clike-OWQBSOGL.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/clojure-KV75UQWC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/cmake-GTEP5Y3C.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/cobol-WCZ7QEGJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/coffeescript-TGLBOT6B.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/commonlisp-WVVI432V.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/crystal-S6R6HLKQ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/css-PDA2RINP.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/cypher-HL5QPL4C.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/d-HHJH2HAJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/diff-MN3A35BY.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-25SQLBVS.js +3 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-27ZGGJJS.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-4E52OJIF.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-6FT73XDC.js +2 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-BE6YGRMW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-CJSGKW2O.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-CRPH2WLI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-CTG7JEQN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-I3FEVELX.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-IM5BBSWZ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-KERUKTIF.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-LXTMWZAM.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-MABASSET.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-MJVBUZLL.js +3 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-MNB3WZVU.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-QYWXOE3U.js +7 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-TEBOFWLZ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-UNXFROM3.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-XL2SLQO6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-XO42Y3AP.js +6 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-Y2WWDWJK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-YFEZKWMU.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-ZY7SWQO6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dockerfile-3Y4R4NCH.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dtd-5343GJ4P.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dylan-FI2CXSW2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ebnf-GNQ77YWA.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ecl-MRPUAE2L.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/eiffel-DZROES27.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/elm-4KICRFKQ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/erlang-3IHYC263.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/factor-C6D3E5UK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/fcl-VYTWUJMI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/forth-ZBO2EH5M.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/fortran-5NDCRWIK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/gas-SKRMOXFP.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/gherkin-L45YXZLW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/groovy-GLPTLN44.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/haskell-E7NF7P4Y.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/haxe-K2NVDOB6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/http-Q6DRQIJB.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/idl-MZYNJVLN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/javascript-6YRYK2HR.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/julia-HXEL2T5G.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/livescript-MV7CI2JZ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/lua-E36NHOOD.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mathematica-3XOZZCMC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mbox-ODYGFKS7.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mirc-WB43AWF5.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mllike-ZT6ET7ZH.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/modelica-LLMZPFGA.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mscgen-2OPNNYFH.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mumps-6P3HWJH6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/nginx-TCGKQTRW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/nsis-ATIGIXTJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ntriples-5PLBI5OV.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/octave-QF2S3NL6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/oz-JFIVFFH2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/pascal-JG6SW76V.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/perl-4B3IWVVO.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/pig-ABEBH4MI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/powershell-H32XE7TL.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/properties-NN644MMA.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/protobuf-YKWQK2J3.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/pug-RIYXEKFW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/puppet-JZQ5WPEV.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/python-EBCGGHAC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/q-5UMH2YQY.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/r-7XMGG453.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/rpm-IEV7TSLR.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ruby-T4SPFPNM.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sas-GEDX67ET.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/scheme-FTLOWKPC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/shell-HUFRKFSJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sieve-P5QUDVYO.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/smalltalk-2SORJNQI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/solr-M22TGAR5.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sparql-4M3KL752.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/spreadsheet-3K23LA4N.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sql-VHRFMUBI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/stex-QZDAQUY2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/stylus-7ZDKAKBD.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/swift-WCLCLGMR.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/tcl-JLMU6WOK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/textile-4CHDKZ4T.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/tiddlywiki-DKMCZZQ4.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/tiki-F4JIG36Q.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/toml-BHMHTIM2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/troff-YQ2KV5NT.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ttcn-ZHRCVFJY.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ttcn-cfg-KJOVTY4X.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/turtle-535P5N6I.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/vb-76UMQHOT.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/vbscript-CGNDTK6Z.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/velocity-VSJDO5T5.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/verilog-4T6ETXIP.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/vhdl-T7FNAXUN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/webidl-P6MJSJ6J.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/xquery-UOVILRGN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/yacas-CV2APROL.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/z80-3KFNVM4M.js +1 -0
- package/neural-interface/public/vendor/codemirror/codemirror-6.43.14-75f05e67.min.js +2 -0
- package/neural-interface/public/vendor/codemirror/manifest.json +60 -0
- package/neural-interface/scripts/build-vendor.mjs +78 -2
- package/neural-interface/scripts/rebuild-pty.js +15 -12
- package/neural-interface/scripts/vendor-codemirror-entry.js +121 -0
- package/neural-interface/server.js +1092 -1212
- package/neural-interface/templates/rulesets/README.md +1 -1
- package/package.json +13 -7
- package/postinstall.js +11 -81
- package/setup.js +111 -6
- package/benchmarks/browser-live-results.json +0 -79
- package/benchmarks/browser-v2-results.json +0 -258
- package/docs/browser-v2.md +0 -109
- package/docs/hook-integrity.md +0 -89
- package/docs/judgments.md +0 -182
- package/docs/style-guide.md +0 -246
- package/neural-interface/lib/codex-capabilities.d.ts +0 -41
- package/neural-interface/public/category-logos/claude.svg +0 -1
- package/neural-interface/public/category-logos/gemini-color.svg +0 -1
- package/neural-interface/public/category-logos/openai.svg +0 -1
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude.svg +0 -1
- package/neural-interface/public/gallery.svg +0 -6
- package/neural-interface/public/leonardoai.svg +0 -1
- package/neural-interface/public/mcp.svg +0 -4
- package/neural-interface/public/opencode-logo-dark-square.svg +0 -1
- package/neural-interface/public/pin-svgrepo-com.svg +0 -25
- package/neural-interface/public/sessionicon.svg +0 -8
- package/neural-interface/public/toolbar-album.svg +0 -8
- package/neural-interface/public/toolbar-question.svg +0 -6
- package/neural-interface/public/toolbar-sessions.svg +0 -6
- package/neural-interface/scripts/codex-protocol-fixture.mjs +0 -83
- package/scripts/audit-claude-hook-usage.mjs +0 -12
- package/scripts/audit-memory-duplicates.mjs +0 -44
- package/scripts/measure-claude-hook-overhead.mjs +0 -77
- package/scripts/recover-user-config.mjs +0 -254
- package/skills/synabun/synabunicon.png +0 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun Neural Interface — Code editor: unsaved work
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// What keeps an edit from being lost: the browser's own question when the page
|
|
6
|
+
// is left with unsaved text, a copy of every unsaved buffer written a second
|
|
7
|
+
// after the last change (and removed when the buffer is saved or discarded),
|
|
8
|
+
// the check of open files against the disk, and what the bars about all that
|
|
9
|
+
// say. editor-panel.js owns the documents and draws the bars; everything here
|
|
10
|
+
// is handed what it works with (a store, a timer, a way to ask the server), so
|
|
11
|
+
// `node --test` loads it as it is. No imports.
|
|
12
|
+
//
|
|
13
|
+
// Where a copy is kept: the UI blob store's `editor-backups` namespace, which
|
|
14
|
+
// only the owner can read or write, on a server that has it (the `backups`
|
|
15
|
+
// capability). The namespaces of a server that was not restarted are fixed and
|
|
16
|
+
// the only free one is open to invited guests, so until then the copy stays in
|
|
17
|
+
// this browser (IndexedDB).
|
|
18
|
+
|
|
19
|
+
/** A buffer is copied this long after its last change. */
|
|
20
|
+
export const BACKUP_DELAY_MS = 1000;
|
|
21
|
+
/** Buffers longer than this are not copied (1 MB of text). */
|
|
22
|
+
export const BACKUP_MAX_CHARS = 1024 * 1024;
|
|
23
|
+
export const BACKUP_VERSION = 1;
|
|
24
|
+
export const BACKUP_NAMESPACE = 'editor-backups';
|
|
25
|
+
/** What the store keeps: the newest copies, up to this many and this much text. */
|
|
26
|
+
export const BACKUP_KEEP = Object.freeze({ maxEntries: 40, maxChars: 24 * 1024 * 1024 });
|
|
27
|
+
/** Open files are checked against the disk this often while the panel is visible. */
|
|
28
|
+
export const DISK_POLL_MS = 2000;
|
|
29
|
+
/** Paths per request (the server's limit). */
|
|
30
|
+
export const STAT_BATCH = 50;
|
|
31
|
+
|
|
32
|
+
function fnv1a(text, seed = 0x811c9dc5) {
|
|
33
|
+
let hash = seed;
|
|
34
|
+
for (let i = 0; i < text.length; i++) {
|
|
35
|
+
hash ^= text.charCodeAt(i);
|
|
36
|
+
hash = Math.imul(hash, 0x01000193);
|
|
37
|
+
}
|
|
38
|
+
return (hash >>> 0).toString(16).padStart(8, '0');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A short mark of a text: two texts with the same mark are taken to be the same text. */
|
|
42
|
+
export function textFingerprint(text) {
|
|
43
|
+
const value = String(text ?? '');
|
|
44
|
+
return `${value.length}:${fnv1a(value)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The name a document's copy is stored under: safe in an address, the same for the same document. */
|
|
48
|
+
export function backupId(key) {
|
|
49
|
+
const value = String(key ?? '');
|
|
50
|
+
return `b${fnv1a(value)}${fnv1a(value, 0x9747b28c)}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Whether a stored value is a copy of this document. */
|
|
54
|
+
export function isBackup(record, key) {
|
|
55
|
+
return !!record && typeof record === 'object' && record.v === BACKUP_VERSION && record.key === key && typeof record.text === 'string';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* What a copy found when a file is opened means.
|
|
60
|
+
* @param {{ text: string, base?: string } | null} record the copy
|
|
61
|
+
* @param {string} diskText the file as it was just read (line breaks as "\n")
|
|
62
|
+
* @returns {'none' | 'same' | 'restore' | 'restore-changed'} `same`: the file already holds that text, the copy
|
|
63
|
+
* is of no use; `restore`: the unsaved text comes back; `restore-changed`: it comes back, and the file is no
|
|
64
|
+
* longer the one the text was written over
|
|
65
|
+
*/
|
|
66
|
+
export function describeBackup(record, diskText) {
|
|
67
|
+
if (!record) return 'none';
|
|
68
|
+
if (record.text === diskText) return 'same';
|
|
69
|
+
return typeof record.base === 'string' && record.base !== textFingerprint(diskText) ? 'restore-changed' : 'restore';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The copies of unsaved buffers.
|
|
74
|
+
* @param {object} o
|
|
75
|
+
* @param {{ get: (id: string) => Promise<any>, put: (id: string, value: object, o?: { keep?: boolean }) => any, remove: (id: string) => any, flush?: (o?: object) => any }} o.store
|
|
76
|
+
* `put` may answer a promise that says whether the copy has arrived (`false`: it has not); a store that answers
|
|
77
|
+
* nothing is taken at its word. `keep: false` marks a copy that only takes the place of one that goes (a file
|
|
78
|
+
* was renamed): the store evicts nothing for it.
|
|
79
|
+
* @param {number} [o.delayMs]
|
|
80
|
+
* @param {number} [o.maxChars]
|
|
81
|
+
* @param {() => number} [o.now]
|
|
82
|
+
* @param {(fn: () => void, ms: number) => any} [o.setTimer]
|
|
83
|
+
* @param {(timer: any) => void} [o.clearTimer]
|
|
84
|
+
*/
|
|
85
|
+
export function createBackups({
|
|
86
|
+
store, delayMs = BACKUP_DELAY_MS, maxChars = BACKUP_MAX_CHARS, now = () => Date.now(),
|
|
87
|
+
setTimer = (fn, ms) => setTimeout(fn, ms), clearTimer = (timer) => clearTimeout(timer),
|
|
88
|
+
}) {
|
|
89
|
+
const waiting = new Map(); // key → { timer, read }: a copy is due
|
|
90
|
+
const stored = new Set(); // keys that may have a copy in the store
|
|
91
|
+
// The copies that are on their way from one key to another (a file was renamed while its unsaved text was only
|
|
92
|
+
// in the store): { from, to, path, since, commit, fresh, reused }
|
|
93
|
+
// since when the file was renamed: only a copy written before that is this document's
|
|
94
|
+
// commit what is remembered of the document may follow it (called once, when its copy is under `to`)
|
|
95
|
+
// fresh a newer copy is under `to` by now, or none is wanted there: the one on its way is not brought
|
|
96
|
+
// reused this page wrote another document's copy under `from` since: nothing there is this document's
|
|
97
|
+
const jobs = new Set();
|
|
98
|
+
let queue = Promise.resolve(); // one after the other: the second of two renames finds what the first put
|
|
99
|
+
|
|
100
|
+
const usable = (snapshot) => !!snapshot && typeof snapshot.text === 'string' && snapshot.text.length <= maxChars;
|
|
101
|
+
const recordOf = (key, snapshot) => ({
|
|
102
|
+
v: BACKUP_VERSION, key, path: String(snapshot.path ?? ''), text: snapshot.text,
|
|
103
|
+
base: typeof snapshot.base === 'string' ? snapshot.base : null, eol: snapshot.eol === 'CRLF' ? 'CRLF' : 'LF', at: now(),
|
|
104
|
+
});
|
|
105
|
+
// The document at `key` has a newer copy there now, or wants none: a copy on its way to that key is not brought.
|
|
106
|
+
const supersede = (key) => { for (const job of jobs) if (job.to === key) job.fresh = true; };
|
|
107
|
+
const settle = (job) => {
|
|
108
|
+
const { commit } = job;
|
|
109
|
+
job.commit = null;
|
|
110
|
+
if (commit) { try { commit(); } catch { /* the caller's own failure */ } }
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
function write(key) {
|
|
114
|
+
const entry = waiting.get(key);
|
|
115
|
+
if (!entry) return false;
|
|
116
|
+
waiting.delete(key);
|
|
117
|
+
clearTimer(entry.timer);
|
|
118
|
+
let snapshot = null;
|
|
119
|
+
try { snapshot = entry.read(); } catch { snapshot = null; }
|
|
120
|
+
// A buffer that outgrew the limit is not copied; the copy made while it was smaller stays, as the last one there is.
|
|
121
|
+
if (!usable(snapshot)) return false;
|
|
122
|
+
supersede(key); // what is written now is newer than a copy on its way here
|
|
123
|
+
for (const job of jobs) if (job.from === key) job.reused = true; // and what is under this key is this document's
|
|
124
|
+
stored.add(key);
|
|
125
|
+
store.put(backupId(key), recordOf(key, snapshot));
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// One renamed document's copy, brought from the key it was under to the key the document has now. In this
|
|
130
|
+
// order, so that a page that goes at any moment finds the document and its copy under one name: the copy is put
|
|
131
|
+
// under the new key (the store evicts nothing for it); when it has arrived, what is remembered of the document
|
|
132
|
+
// follows (`commit`); then the copy under the old key goes. A copy that was written after the rename is another
|
|
133
|
+
// file's, under a name that was free again: it is neither brought nor removed.
|
|
134
|
+
async function carry(job) {
|
|
135
|
+
let carried = false;
|
|
136
|
+
try {
|
|
137
|
+
let record = null;
|
|
138
|
+
try { record = await store.get(backupId(job.from)); } catch { record = null; }
|
|
139
|
+
const mine = !job.reused && isBackup(record, job.from) && (Number(record.at) || 0) <= job.since;
|
|
140
|
+
if (mine && !job.fresh) {
|
|
141
|
+
let arrived;
|
|
142
|
+
try { arrived = await store.put(backupId(job.to), { ...record, key: job.to, path: String(job.path ?? record.path ?? '') }, { keep: false }); } catch { arrived = false; }
|
|
143
|
+
if (arrived === false && !job.fresh) return false; // it stays where it is, and so does what is remembered
|
|
144
|
+
if (!job.fresh) { stored.add(job.to); carried = true; }
|
|
145
|
+
}
|
|
146
|
+
settle(job);
|
|
147
|
+
if (mine && !job.reused) { stored.delete(job.from); try { store.remove(backupId(job.from)); } catch { /* the store's own failure */ } }
|
|
148
|
+
} catch { carried = false; } finally { jobs.delete(job); }
|
|
149
|
+
return carried;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Where the copy of the document at `key` is by now: under its key, or under the key a move that is still
|
|
153
|
+
// queued will bring it from (through every such move: of two renames in a row, under the first name).
|
|
154
|
+
// `since`: only a copy written before that is the document's; `path`: where the document is.
|
|
155
|
+
function whereabouts(key) {
|
|
156
|
+
let at = key; let path = null; let since = Infinity;
|
|
157
|
+
for (let hop = 0; hop <= jobs.size; hop++) {
|
|
158
|
+
let job = null;
|
|
159
|
+
for (const each of jobs) if (each.to === at && !each.fresh && !each.reused) job = each;
|
|
160
|
+
if (!job) break;
|
|
161
|
+
if (path === null) path = job.path;
|
|
162
|
+
since = Math.min(since, job.since);
|
|
163
|
+
at = job.from;
|
|
164
|
+
}
|
|
165
|
+
return { at, path, since };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
/**
|
|
170
|
+
* A buffer with unsaved text changed: copy it once it has been still for a second.
|
|
171
|
+
* @param {string} key the document (its tab key)
|
|
172
|
+
* @param {() => ({ path: string, text: string, base?: string, eol?: string } | null)} read the buffer when the
|
|
173
|
+
* copy is made; null when there is nothing to copy any more
|
|
174
|
+
*/
|
|
175
|
+
touch(key, read) {
|
|
176
|
+
const entry = waiting.get(key);
|
|
177
|
+
if (entry) clearTimer(entry.timer);
|
|
178
|
+
waiting.set(key, { read, timer: setTimer(() => write(key), delayMs) });
|
|
179
|
+
},
|
|
180
|
+
/**
|
|
181
|
+
* The buffer was saved or discarded: no copy is due, and the one in the store goes.
|
|
182
|
+
* @param {string} key
|
|
183
|
+
* @param {{ always?: boolean }} [o] `always`: ask the store even when this page never saw a copy (a tab that
|
|
184
|
+
* came back with unsaved text and was closed before it was read)
|
|
185
|
+
*/
|
|
186
|
+
drop(key, { always = false } = {}) {
|
|
187
|
+
const entry = waiting.get(key);
|
|
188
|
+
if (entry) { clearTimer(entry.timer); waiting.delete(key); }
|
|
189
|
+
supersede(key);
|
|
190
|
+
if (!stored.delete(key) && !always) return false;
|
|
191
|
+
store.remove(backupId(key));
|
|
192
|
+
return true;
|
|
193
|
+
},
|
|
194
|
+
/**
|
|
195
|
+
* The document has another key now (its file was renamed or moved): its copy is kept under the new key, with
|
|
196
|
+
* the new path, and nothing is left under the old one. A copy that is due is made under the new key.
|
|
197
|
+
*
|
|
198
|
+
* The text of an open document (`read`) is the newest there is: it is stored under the new key in this very
|
|
199
|
+
* call, the copy under the old key goes, and `commit` is called before the call returns. A copy that is only
|
|
200
|
+
* in the store (a kept tab that is not open) has to be read from there first: it is brought by a queue, one
|
|
201
|
+
* move after the other, and `commit` is called when it is under the new key (see `carry`). Until then nothing
|
|
202
|
+
* is added to the store and nothing is remembered anew, so a page that leaves in between has lost nothing: the
|
|
203
|
+
* document is remembered where its copy still is. Of two quick renames the copy ends under the last name, and
|
|
204
|
+
* nothing stays under the one in between.
|
|
205
|
+
* @param {string} from @param {string} to the document's key before and now @param {string} path where it is now
|
|
206
|
+
* @param {object} [o]
|
|
207
|
+
* @param {() => ({ path: string, text: string, base?: string, eol?: string } | null)} [o.read] the buffer as it
|
|
208
|
+
* is now, when the document is open with unsaved text (null otherwise)
|
|
209
|
+
* @param {boolean} [o.expected] the document is known to have unsaved text, so a copy is expected in the store
|
|
210
|
+
* @param {number} [o.since] when the file was renamed (now, unless the caller had to wait before it could ask)
|
|
211
|
+
* @param {() => void} [o.commit] what is remembered of the document may follow it to the new key. Called once:
|
|
212
|
+
* in the call when there is nothing to wait for, else when the copy has arrived under the new key (or there
|
|
213
|
+
* turned out to be none of this document's). Not called when the copy could not be put there.
|
|
214
|
+
* @returns {Promise<boolean>} whether a stored copy was carried over
|
|
215
|
+
*/
|
|
216
|
+
move(from, to, path, { read = null, expected = false, since = now(), commit = null } = {}) {
|
|
217
|
+
const done = () => { if (commit) { try { commit(); } catch { /* the caller's own failure */ } } };
|
|
218
|
+
if (from === to) { done(); return Promise.resolve(false); }
|
|
219
|
+
const entry = waiting.get(from);
|
|
220
|
+
if (entry) {
|
|
221
|
+
clearTimer(entry.timer);
|
|
222
|
+
waiting.delete(from);
|
|
223
|
+
const there = waiting.get(to);
|
|
224
|
+
if (there) clearTimer(there.timer);
|
|
225
|
+
waiting.set(to, { read: entry.read, timer: setTimer(() => write(to), delayMs) });
|
|
226
|
+
}
|
|
227
|
+
const known = stored.delete(from);
|
|
228
|
+
// A document that was on its way to this key gave it up: its tab was closed for the one that takes it.
|
|
229
|
+
supersede(to);
|
|
230
|
+
let snapshot = null;
|
|
231
|
+
if (read) { try { snapshot = read(); } catch { snapshot = null; } }
|
|
232
|
+
if (usable(snapshot)) {
|
|
233
|
+
let put = true;
|
|
234
|
+
try { store.put(backupId(to), recordOf(to, snapshot)); } catch { put = false; }
|
|
235
|
+
if (put) {
|
|
236
|
+
stored.add(to);
|
|
237
|
+
supersede(from); // a copy that was still on its way to the key that was left is older than this text
|
|
238
|
+
if (known || expected) { try { store.remove(backupId(from)); } catch { /* the store's own failure */ } }
|
|
239
|
+
done();
|
|
240
|
+
return Promise.resolve(false);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// What is remembered waits only for a copy that is expected; one this page knows nothing of is brought all the same.
|
|
244
|
+
const job = { from, to, path, since, commit: known || expected ? commit : null, fresh: false, reused: false };
|
|
245
|
+
if (!job.commit) done();
|
|
246
|
+
jobs.add(job);
|
|
247
|
+
const step = queue.then(() => carry(job));
|
|
248
|
+
queue = step.then(() => {}, () => {});
|
|
249
|
+
return step;
|
|
250
|
+
},
|
|
251
|
+
/**
|
|
252
|
+
* Make every copy that is due now (the page is being hidden or left). A move that is still under way needs
|
|
253
|
+
* nothing more: its copy and what is remembered of its document are together at every step.
|
|
254
|
+
*/
|
|
255
|
+
flush({ leaving = false } = {}) {
|
|
256
|
+
let wrote = 0;
|
|
257
|
+
for (const key of [...waiting.keys()]) if (write(key)) wrote++;
|
|
258
|
+
store.flush?.({ leaving });
|
|
259
|
+
return wrote;
|
|
260
|
+
},
|
|
261
|
+
/**
|
|
262
|
+
* The copy of a document, or null. Nothing is waited for here: opening a file must not hang on the queue (a
|
|
263
|
+
* write of another file's copy that does not arrive would hold every file up). A copy that is still on its
|
|
264
|
+
* way to this key is read from where it is by now, and handed over as this document's.
|
|
265
|
+
*/
|
|
266
|
+
async read(key) {
|
|
267
|
+
const tried = new Set();
|
|
268
|
+
for (;;) {
|
|
269
|
+
const { at, path, since } = whereabouts(key);
|
|
270
|
+
if (tried.has(at)) return null; // it is under none of the keys it could be under
|
|
271
|
+
tried.add(at);
|
|
272
|
+
let record = null;
|
|
273
|
+
try { record = await store.get(backupId(at)); } catch { record = null; }
|
|
274
|
+
if (at === key) {
|
|
275
|
+
if (!isBackup(record, key)) return null;
|
|
276
|
+
stored.add(key);
|
|
277
|
+
return record;
|
|
278
|
+
}
|
|
279
|
+
if (isBackup(record, at) && (Number(record.at) || 0) <= since) return { ...record, key, path: String(path ?? record.path ?? '') };
|
|
280
|
+
// Not there: it was carried on while this was asked for, or there is none. Look where it is now.
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
/** Whether a copy of this document is due. */
|
|
284
|
+
due: (key) => waiting.has(key),
|
|
285
|
+
/** Whether the store may hold a copy of this document. */
|
|
286
|
+
has: (key) => stored.has(key),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Copies in the UI blob store (public/shared/blob-store.js), in a namespace of their own.
|
|
292
|
+
* @param {{ fetchBlob: Function, putBlob: Function, deleteBlob: Function, flushBlobs: Function, namespace?: string }} o
|
|
293
|
+
*/
|
|
294
|
+
export function blobBackupStore({ fetchBlob, putBlob, deleteBlob, flushBlobs, namespace = BACKUP_NAMESPACE }) {
|
|
295
|
+
return {
|
|
296
|
+
get: (id) => fetchBlob(namespace, id),
|
|
297
|
+
// Answers what putBlob answers: whether the write has arrived. A copy that only takes another's place
|
|
298
|
+
// (`keep: false`) names no limits, so the store evicts nothing for it.
|
|
299
|
+
put: (id, value, { keep = true } = {}) => putBlob(namespace, id, value, keep ? { debounceMs: 0, ...BACKUP_KEEP } : { debounceMs: 0 }),
|
|
300
|
+
// Tried once. A copy the server could not remove comes back when its file is opened again: as unsaved text
|
|
301
|
+
// under the "restored" bar, which an undo takes out (a copy that equals the file is dropped unseen).
|
|
302
|
+
remove: (id) => deleteBlob(namespace, id),
|
|
303
|
+
flush: ({ leaving = false } = {}) => flushBlobs({ useBeacon: leaving }),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Copies kept in this browser, for a server whose blob store has no place for them yet.
|
|
309
|
+
* @param {{ indexedDB?: IDBFactory, name?: string, maxEntries?: number }} [o]
|
|
310
|
+
*/
|
|
311
|
+
export function localBackupStore({ indexedDB = globalThis.indexedDB, name = 'synabun-editor', maxEntries = BACKUP_KEEP.maxEntries } = {}) {
|
|
312
|
+
const TABLE = 'backups';
|
|
313
|
+
let opening = null;
|
|
314
|
+
const database = () => {
|
|
315
|
+
if (!opening) {
|
|
316
|
+
opening = new Promise((resolve, reject) => {
|
|
317
|
+
if (!indexedDB) { reject(new Error('IndexedDB is not available')); return; }
|
|
318
|
+
const request = indexedDB.open(name, 1);
|
|
319
|
+
request.onupgradeneeded = () => { if (!request.result.objectStoreNames.contains(TABLE)) request.result.createObjectStore(TABLE); };
|
|
320
|
+
request.onsuccess = () => resolve(request.result);
|
|
321
|
+
request.onerror = () => reject(request.error);
|
|
322
|
+
request.onblocked = () => reject(new Error('IndexedDB is blocked'));
|
|
323
|
+
});
|
|
324
|
+
opening.catch(() => { opening = null; });
|
|
325
|
+
}
|
|
326
|
+
return opening;
|
|
327
|
+
};
|
|
328
|
+
const run = async (mode, work) => {
|
|
329
|
+
const db = await database();
|
|
330
|
+
return new Promise((resolve, reject) => {
|
|
331
|
+
const tx = db.transaction(TABLE, mode);
|
|
332
|
+
const request = work(tx.objectStore(TABLE));
|
|
333
|
+
tx.oncomplete = () => resolve(request?.result ?? null);
|
|
334
|
+
tx.onerror = () => reject(tx.error);
|
|
335
|
+
tx.onabort = () => reject(tx.error);
|
|
336
|
+
});
|
|
337
|
+
};
|
|
338
|
+
// The oldest copies go once there are more than the store keeps.
|
|
339
|
+
const prune = async () => {
|
|
340
|
+
const keys = await run('readonly', (table) => table.getAllKeys());
|
|
341
|
+
if (!keys || keys.length <= maxEntries) return;
|
|
342
|
+
const values = await run('readonly', (table) => table.getAll());
|
|
343
|
+
const oldest = keys.map((key, i) => ({ key, at: Number(values?.[i]?.at) || 0 })).sort((a, b) => b.at - a.at).slice(maxEntries);
|
|
344
|
+
await run('readwrite', (table) => { for (const entry of oldest) table.delete(entry.key); return null; });
|
|
345
|
+
};
|
|
346
|
+
return {
|
|
347
|
+
get: (id) => run('readonly', (table) => table.get(id)).catch(() => null),
|
|
348
|
+
// Answers whether the copy is written. `keep: false`: it takes the place of one that goes, nothing is pruned for it.
|
|
349
|
+
put: (id, value, { keep = true } = {}) => run('readwrite', (table) => table.put(value, id)).then(() => (keep ? prune().catch(() => {}) : null)).then(() => true, () => false),
|
|
350
|
+
remove: (id) => run('readwrite', (table) => table.delete(id)).catch(() => {}),
|
|
351
|
+
flush() {},
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* One store over several: a copy is written to the first; it is looked for in each, in order, and removed from
|
|
357
|
+
* all. The later ones are where copies were kept before (a server that has since been restarted).
|
|
358
|
+
*/
|
|
359
|
+
export function layeredBackupStore(primary, ...older) {
|
|
360
|
+
const all = [primary, ...older].filter(Boolean);
|
|
361
|
+
return {
|
|
362
|
+
async get(id) {
|
|
363
|
+
for (const store of all) {
|
|
364
|
+
let value = null;
|
|
365
|
+
try { value = await store.get(id); } catch { value = null; }
|
|
366
|
+
if (value) return value;
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
},
|
|
370
|
+
put: (id, value, o) => primary.put(id, value, o),
|
|
371
|
+
remove(id) { for (const store of all) { try { store.remove(id); } catch { /* that store's own failure */ } } },
|
|
372
|
+
flush: (o) => primary.flush?.(o),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* The browser's question when the page is left while something is unsaved.
|
|
378
|
+
* @param {object} o
|
|
379
|
+
* @param {EventTarget} o.target the window
|
|
380
|
+
* @param {() => boolean} o.isDirty
|
|
381
|
+
* @param {() => void} [o.beforeLeave] runs first, whatever the answer: what must be written before the page goes
|
|
382
|
+
*/
|
|
383
|
+
export function createUnloadGuard({ target, isDirty, beforeLeave = () => {} }) {
|
|
384
|
+
const onBeforeUnload = (event) => {
|
|
385
|
+
try { beforeLeave(); } catch { /* leaving is not held up by it */ }
|
|
386
|
+
let dirty = false;
|
|
387
|
+
try { dirty = !!isDirty(); } catch { dirty = false; }
|
|
388
|
+
if (!dirty) return undefined;
|
|
389
|
+
event.preventDefault();
|
|
390
|
+
event.returnValue = '';
|
|
391
|
+
return '';
|
|
392
|
+
};
|
|
393
|
+
return {
|
|
394
|
+
install() { target.addEventListener('beforeunload', onBeforeUnload); },
|
|
395
|
+
remove() { target.removeEventListener('beforeunload', onBeforeUnload); },
|
|
396
|
+
handler: onBeforeUnload,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Whether two file versions (`{ mtimeMs, size }`) are the same. Without both, they are not. */
|
|
401
|
+
export function sameVersion(a, b) {
|
|
402
|
+
return !!a && !!b && a.mtimeMs === b.mtimeMs && a.size === b.size;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* The check of open files against the disk.
|
|
407
|
+
* @param {object} o
|
|
408
|
+
* @param {(paths: string[]) => Promise<{ [path: string]: { exists: boolean, version?: object } } | null>} o.stat
|
|
409
|
+
* asks the server (POST /api/file-stat); null or a rejection means no answer this time
|
|
410
|
+
* @param {() => { id: string, path: string, version: object | null }[]} o.files the open files and the version
|
|
411
|
+
* each was read or saved at
|
|
412
|
+
* @param {(id: string, change: { exists: boolean, version: object | null, asked: object | null, path: string }) => void} o.onChange
|
|
413
|
+
* a file is not what it was: `asked` is the version the check compared with, `path` the path it was asked for
|
|
414
|
+
* @param {() => boolean} [o.isActive] whether a check is worth making now (the panel is visible)
|
|
415
|
+
* @param {number} [o.intervalMs]
|
|
416
|
+
*/
|
|
417
|
+
export function createDiskWatcher({
|
|
418
|
+
stat, files, onChange, isActive = () => true, intervalMs = DISK_POLL_MS,
|
|
419
|
+
setTimer = (fn, ms) => setTimeout(fn, ms), clearTimer = (timer) => clearTimeout(timer),
|
|
420
|
+
}) {
|
|
421
|
+
let timer = null;
|
|
422
|
+
let running = false;
|
|
423
|
+
let checking = null;
|
|
424
|
+
|
|
425
|
+
async function checkOnce() {
|
|
426
|
+
const open = files().filter((file) => file && file.path);
|
|
427
|
+
for (let at = 0; at < open.length; at += STAT_BATCH) {
|
|
428
|
+
const batch = open.slice(at, at + STAT_BATCH);
|
|
429
|
+
let answer = null;
|
|
430
|
+
try { answer = await stat(batch.map((file) => file.path)); } catch { answer = null; }
|
|
431
|
+
if (!answer) return false;
|
|
432
|
+
for (const file of batch) {
|
|
433
|
+
const entry = answer[file.path];
|
|
434
|
+
if (!entry) continue;
|
|
435
|
+
if (entry.exists === false) {
|
|
436
|
+
if (file.version) onChange(file.id, { exists: false, version: null, asked: file.version, path: file.path });
|
|
437
|
+
} else if (!sameVersion(entry.version, file.version)) {
|
|
438
|
+
onChange(file.id, { exists: true, version: entry.version || null, asked: file.version, path: file.path });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Check now. One check runs at a time: a second call gets the one under way. */
|
|
446
|
+
function check() {
|
|
447
|
+
if (!checking) checking = checkOnce().finally(() => { checking = null; });
|
|
448
|
+
return checking;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function schedule() {
|
|
452
|
+
if (timer !== null) clearTimer(timer);
|
|
453
|
+
timer = null;
|
|
454
|
+
if (!running) return;
|
|
455
|
+
timer = setTimer(async () => {
|
|
456
|
+
timer = null;
|
|
457
|
+
if (running && isActive()) await check().catch(() => {});
|
|
458
|
+
schedule();
|
|
459
|
+
}, intervalMs);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
start() { if (!running) { running = true; schedule(); } },
|
|
464
|
+
stop() { running = false; if (timer !== null) clearTimer(timer); timer = null; },
|
|
465
|
+
check,
|
|
466
|
+
running: () => running,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* What a bar about unsaved work says and offers. The panel translates the keys and gives each action its function.
|
|
472
|
+
* @param {'conflict' | 'changed' | 'deleted' | 'restored' | 'restored-changed' | 'comparing'} kind
|
|
473
|
+
* `conflict`: a save was refused because the file changed on disk; `changed`: the check found the file changed
|
|
474
|
+
* while the buffer has unsaved text; `deleted`: the file is gone; `restored`: unsaved text came back from a
|
|
475
|
+
* copy; `restored-changed`: it came back over a file that changed since; `comparing`: the comparison is showing
|
|
476
|
+
* @param {{ canCompare?: boolean }} [o] the comparison can be shown (the server has what it takes)
|
|
477
|
+
* @returns {{ id: 'disk' | 'restored' | 'compare', textKey: string, actions: { id: string, labelKey: string }[] }}
|
|
478
|
+
*/
|
|
479
|
+
export function safetyBar(kind, { canCompare = false } = {}) {
|
|
480
|
+
const compare = canCompare ? [{ id: 'compare', labelKey: 'editor.disk.compare' }] : [];
|
|
481
|
+
const reload = { id: 'reload', labelKey: 'editor.conflict.reload' };
|
|
482
|
+
switch (kind) {
|
|
483
|
+
case 'conflict':
|
|
484
|
+
return { id: 'disk', textKey: 'editor.conflict.text', actions: [...compare, reload, { id: 'overwrite', labelKey: 'editor.conflict.overwrite' }] };
|
|
485
|
+
case 'changed':
|
|
486
|
+
return { id: 'disk', textKey: 'editor.disk.changed', actions: [...compare, reload, { id: 'keepMine', labelKey: 'editor.disk.keepMine' }] };
|
|
487
|
+
case 'deleted':
|
|
488
|
+
return { id: 'disk', textKey: 'editor.disk.deleted', actions: [{ id: 'keepMine', labelKey: 'editor.disk.keepOpen' }, { id: 'closeTab', labelKey: 'editor.disk.closeTab' }] };
|
|
489
|
+
case 'restored':
|
|
490
|
+
return { id: 'restored', textKey: 'editor.backup.restored', actions: [{ id: 'keep', labelKey: 'editor.backup.keep' }, { id: 'discard', labelKey: 'editor.backup.discard' }] };
|
|
491
|
+
case 'restored-changed':
|
|
492
|
+
return { id: 'restored', textKey: 'editor.backup.restoredChanged', actions: [...compare, { id: 'keep', labelKey: 'editor.backup.keep' }, { id: 'discard', labelKey: 'editor.backup.discard' }] };
|
|
493
|
+
case 'comparing':
|
|
494
|
+
return { id: 'compare', textKey: 'editor.compare.text', actions: [{ id: 'closeCompare', labelKey: 'editor.compare.close' }] };
|
|
495
|
+
case 'diffing':
|
|
496
|
+
return { id: 'compare', textKey: 'editor.diff.text', actions: [{ id: 'closeDiff', labelKey: 'editor.diff.close' }] };
|
|
497
|
+
default:
|
|
498
|
+
throw new Error(`Unknown bar: ${kind}`);
|
|
499
|
+
}
|
|
500
|
+
}
|