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