synabun 2.0.0 → 2.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/NOTICE +5 -0
- package/README.md +1 -1
- package/THIRD-PARTY-LICENSES.md +46 -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/lib/start-launcher.js +88 -1
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -3
- package/neural-interface/i18n/en.json +1500 -153
- package/neural-interface/i18n/pt-BR.json +1498 -152
- 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-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/browser-background-tab.js +134 -0
- package/neural-interface/lib/browser-screenshot.js +678 -61
- package/neural-interface/lib/claude-agent-bridge.js +33 -6
- package/neural-interface/lib/claude-model-catalog.js +30 -2
- 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-loop-providers.js +10 -5
- 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/ui-blob-api.js +108 -0
- package/neural-interface/lib/ui-blob-store.js +68 -0
- package/neural-interface/package-lock.json +701 -2
- package/neural-interface/package.json +13 -2
- package/neural-interface/public/install-app.html +9 -6
- package/neural-interface/public/offline.html +3 -2
- package/neural-interface/public/onboarding.html +12 -1
- 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 +18 -58
- 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-render.js +162 -19
- 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 +158 -4
- 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/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 +160 -165
- 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 +29 -1
- package/neural-interface/public/shared/pwa-install.css +3 -3
- package/neural-interface/public/shared/pwa-install.js +100 -1
- 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-user-context.js +154 -0
- package/neural-interface/public/shared/start-bridge.js +238 -19
- package/neural-interface/public/shared/styles.css +1414 -721
- package/neural-interface/public/shared/synabun-mascot.js +20 -56
- package/neural-interface/public/shared/tunnel-view.js +167 -0
- package/neural-interface/public/shared/ui-claude-panel.js +157 -16
- 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-keybinds.js +4 -0
- package/neural-interface/public/shared/ui-loading.js +159 -15
- package/neural-interface/public/shared/ui-menubar.js +12 -30
- 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 +455 -488
- 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 +23 -5
- 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 +2 -2
- 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/vendor-codemirror-entry.js +121 -0
- package/neural-interface/server.js +873 -1028
- package/neural-interface/templates/rulesets/README.md +1 -1
- package/package.json +2 -8
- package/setup.js +44 -7
- package/benchmarks/browser-live-results.json +0 -79
- package/benchmarks/browser-v2-results.json +0 -258
- 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/skills/synabun/synabunicon.png +0 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun Neural Interface — File links in agent messages: the decisions
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// A link in an agent's message can point at a file on this computer: a path,
|
|
6
|
+
// a `file:` URL, with or without a place in the file (`:line[:column]`,
|
|
7
|
+
// `#Lline[Ccolumn]`). This module reads such a target and says which paths to
|
|
8
|
+
// try, and decides what a click on one does. ui-file-links.js gives that
|
|
9
|
+
// decision the page (the bus, the server, the toast). The four side panels
|
|
10
|
+
// share both.
|
|
11
|
+
//
|
|
12
|
+
// The text is a model's: nothing here trusts it. A target only ever becomes a
|
|
13
|
+
// path handed to /api/file-content, which keeps its own checks.
|
|
14
|
+
// No imports and no `document`: the click handler only touches the event and
|
|
15
|
+
// the link it is handed. Loaded by `node --test` as it is, and by the Markdown
|
|
16
|
+
// sanitiser.
|
|
17
|
+
|
|
18
|
+
/** The English of `fileLinks.*` in i18n/en.json (a test keeps them equal). */
|
|
19
|
+
export const FILE_LINK_COPY = Object.freeze({
|
|
20
|
+
open: 'Open in editor',
|
|
21
|
+
openAtLine: 'Open in editor at line {line}',
|
|
22
|
+
openAtLineColumn: 'Open in editor at line {line}, column {column}',
|
|
23
|
+
missing: '{name} was not found, or it is a folder.',
|
|
24
|
+
notText: '{name} is not a text file the editor can open.',
|
|
25
|
+
tooLarge: '{name} is too large to open in the editor.',
|
|
26
|
+
outside: '{name} is outside your registered projects. Add its folder as a project to open it here.',
|
|
27
|
+
failed: 'Could not reach SynaBun to open {name}. Check that the server is running.',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/** Longer than any path: a target past this is not read at all. */
|
|
31
|
+
export const MAX_TARGET_LENGTH = 2048;
|
|
32
|
+
|
|
33
|
+
const DRIVE_RE = /^[a-z]:[\\/]/i;
|
|
34
|
+
const SCHEME_RE = /^([a-z][a-z0-9+.-]*):/i;
|
|
35
|
+
// `name.ext:12` and `name.ext:12:3` read as a scheme to a URL parser; they are a file and a place in it.
|
|
36
|
+
const PLACE_ONLY_RE = /^\d+(?::\d+)?(?:#.*)?$/;
|
|
37
|
+
const LINE_SUFFIX_RE = /:(\d+)(?::(\d+))?$/;
|
|
38
|
+
const LINE_HASH_RE = /^#L(\d+)(?:C(\d+))?(?:-L?\d+(?:C\d+)?)?$/i;
|
|
39
|
+
|
|
40
|
+
const slashes = (path) => String(path ?? '').replace(/\\/g, '/');
|
|
41
|
+
|
|
42
|
+
/** How a target reads: a `path`, a `file` URL, a `web` address (http, https), or null for anything else. */
|
|
43
|
+
function kindOf(value) {
|
|
44
|
+
if (!value || value.length > MAX_TARGET_LENGTH) return null;
|
|
45
|
+
if (value.startsWith('#') || value.startsWith('?') || value.startsWith('//')) return null;
|
|
46
|
+
if (DRIVE_RE.test(value)) return 'path';
|
|
47
|
+
const scheme = SCHEME_RE.exec(value);
|
|
48
|
+
if (!scheme) return 'path';
|
|
49
|
+
const name = scheme[1].toLowerCase();
|
|
50
|
+
if (name === 'file') return 'file';
|
|
51
|
+
if (name === 'http' || name === 'https') return 'web';
|
|
52
|
+
const rest = value.slice(scheme[0].length);
|
|
53
|
+
if (rest.startsWith('//')) return null;
|
|
54
|
+
return PLACE_ONLY_RE.test(rest) ? 'path' : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A link target as written, when it reads as a file on this computer rather than an address.
|
|
59
|
+
* The Markdown sanitiser keeps it beside the link (`data-file-target`): the `href` it writes is resolved
|
|
60
|
+
* against the page, which loses a relative path, `~`, a `file:` URL and `name.ext:12`.
|
|
61
|
+
* @param {string} raw the `href` an agent wrote
|
|
62
|
+
* @returns {string | null}
|
|
63
|
+
*/
|
|
64
|
+
export function localFileHref(raw) {
|
|
65
|
+
const value = String(raw ?? '').trim();
|
|
66
|
+
const kind = kindOf(value);
|
|
67
|
+
return kind === 'path' || kind === 'file' ? value : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A path with `.`, `..` and doubled slashes resolved, and no slash at its end. */
|
|
71
|
+
export function normalizeFilePath(path) {
|
|
72
|
+
const value = slashes(path);
|
|
73
|
+
const drive = /^[a-z]:/i.exec(value)?.[0] || '';
|
|
74
|
+
const rest = value.slice(drive.length);
|
|
75
|
+
const rooted = rest.startsWith('/');
|
|
76
|
+
const parts = [];
|
|
77
|
+
for (const part of rest.split('/')) {
|
|
78
|
+
if (!part || part === '.') continue;
|
|
79
|
+
if (part === '..') { if (parts.length && parts[parts.length - 1] !== '..') parts.pop(); else if (!rooted) parts.push('..'); continue; }
|
|
80
|
+
parts.push(part);
|
|
81
|
+
}
|
|
82
|
+
return `${drive}${rooted ? '/' : ''}${parts.join('/')}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const isAbsolute = (path) => path.startsWith('/') || DRIVE_RE.test(path);
|
|
86
|
+
|
|
87
|
+
function decoded(text) {
|
|
88
|
+
try { return decodeURIComponent(text); } catch { return text; }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Read a link target.
|
|
93
|
+
* @param {string} raw the target as written (an `href`, or the text of a path a panel made clickable)
|
|
94
|
+
* @param {{ cwd?: string, home?: string, origin?: string }} [where] `cwd`: the working directory of the tab or
|
|
95
|
+
* session the message belongs to (a relative path is resolved against it); `home`: the user's home folder
|
|
96
|
+
* (for `~`); `origin`: the page's own origin (an address on it is read as the path it holds, which is how a
|
|
97
|
+
* link sanitised before `data-file-target` existed looks)
|
|
98
|
+
* @returns {{ path: string, candidates: string[], line: number | null, column: number | null,
|
|
99
|
+
* needsHome: boolean, sameOrigin: boolean } | null} null when the target is not a file's. `candidates` are
|
|
100
|
+
* the paths to try, in order; `path` is the first. `needsHome`: the target starts with `~` and no home was
|
|
101
|
+
* given, so there is nothing to try yet. `sameOrigin`: the target was an address of this app.
|
|
102
|
+
*/
|
|
103
|
+
export function parseFileLinkTarget(raw, { cwd = '', home = '', origin = '' } = {}) {
|
|
104
|
+
const value = String(raw ?? '').trim();
|
|
105
|
+
const kind = kindOf(value);
|
|
106
|
+
if (!kind) return null;
|
|
107
|
+
|
|
108
|
+
let written;
|
|
109
|
+
let hash = '';
|
|
110
|
+
let sameOrigin = false;
|
|
111
|
+
if (kind === 'path') {
|
|
112
|
+
const at = value.indexOf('#');
|
|
113
|
+
written = at < 0 ? value : value.slice(0, at);
|
|
114
|
+
hash = at < 0 ? '' : value.slice(at);
|
|
115
|
+
} else {
|
|
116
|
+
let url;
|
|
117
|
+
try { url = new URL(value); } catch { return null; }
|
|
118
|
+
if (kind === 'web') {
|
|
119
|
+
if (!origin || url.origin !== origin) return null;
|
|
120
|
+
sameOrigin = true;
|
|
121
|
+
} else if (url.host && url.host !== 'localhost') {
|
|
122
|
+
return null; // a file on another machine
|
|
123
|
+
}
|
|
124
|
+
written = url.pathname;
|
|
125
|
+
hash = url.hash;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// An agent may have escaped the path (a link with a space in it has to be); it may also hold a real "%".
|
|
129
|
+
const spellings = kind === 'path' ? [...new Set([decoded(written), written])] : [decoded(written)];
|
|
130
|
+
// A working directory that is not a whole path would be read from the server's own folder: it is not used.
|
|
131
|
+
const root = isAbsolute(slashes(cwd)) ? normalizeFilePath(cwd) : '';
|
|
132
|
+
const candidates = [];
|
|
133
|
+
let line = null;
|
|
134
|
+
let column = null;
|
|
135
|
+
let needsHome = false;
|
|
136
|
+
for (const spelling of spellings) {
|
|
137
|
+
let path = slashes(spelling);
|
|
138
|
+
const place = LINE_SUFFIX_RE.exec(path);
|
|
139
|
+
if (place) {
|
|
140
|
+
line ??= Number(place[1]) || null;
|
|
141
|
+
column ??= Number(place[2]) || null;
|
|
142
|
+
path = path.slice(0, place.index);
|
|
143
|
+
}
|
|
144
|
+
if (/^\/[a-z]:\//i.test(path)) path = path.slice(1); // file:///C:/…
|
|
145
|
+
if (path === '~' || path.startsWith('~/')) {
|
|
146
|
+
if (!home) { needsHome = true; continue; }
|
|
147
|
+
path = `${slashes(home).replace(/\/+$/, '')}${path.slice(1)}`;
|
|
148
|
+
}
|
|
149
|
+
if (!path) continue;
|
|
150
|
+
if (isAbsolute(path)) {
|
|
151
|
+
candidates.push(normalizeFilePath(path));
|
|
152
|
+
// The sanitiser used to turn `src/a.js` into `<origin>/src/a.js`: the same link, read from its project.
|
|
153
|
+
if (sameOrigin && root) candidates.push(normalizeFilePath(`${root}/${path}`));
|
|
154
|
+
} else if (root) {
|
|
155
|
+
candidates.push(normalizeFilePath(`${root}/${path}`));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const place = LINE_HASH_RE.exec(hash);
|
|
160
|
+
if (place) {
|
|
161
|
+
line = Number(place[1]) || line;
|
|
162
|
+
column = Number(place[2]) || (Number(place[1]) ? null : column);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const unique = [...new Set(candidates.filter((path) => path && path !== '/'))];
|
|
166
|
+
if (!unique.length && !needsHome) return null;
|
|
167
|
+
return { path: unique[0] || '', candidates: unique, line, column, needsHome: needsHome && !unique.length, sameOrigin };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Whether `path` is one of `roots` or inside one. */
|
|
171
|
+
export function isInsideRoots(path, roots = []) {
|
|
172
|
+
const target = normalizeFilePath(path);
|
|
173
|
+
if (!isAbsolute(target)) return false;
|
|
174
|
+
for (const root of roots) {
|
|
175
|
+
const base = root ? normalizeFilePath(root) : '';
|
|
176
|
+
if (!base || !isAbsolute(base)) continue;
|
|
177
|
+
if (target === base || target.startsWith(base.endsWith('/') ? base : `${base}/`)) return true;
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The last part of a path: the name a notice uses. */
|
|
183
|
+
export function fileLinkName(path) {
|
|
184
|
+
const value = slashes(path).replace(/\/+$/, '');
|
|
185
|
+
return value.slice(value.lastIndexOf('/') + 1) || value;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** A path as a panel copies it: without the place in the file. */
|
|
189
|
+
export function withoutPlace(text) {
|
|
190
|
+
return String(text ?? '').trim().replace(/:\d+(?::\d+)?$/, '');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The tooltip of a link the editor may open.
|
|
195
|
+
* @param {{ line?: number | null, column?: number | null }} target from parseFileLinkTarget()
|
|
196
|
+
* @returns {{ key: 'open' | 'openAtLine' | 'openAtLineColumn', params: object }}
|
|
197
|
+
*/
|
|
198
|
+
export function fileLinkTitle(target) {
|
|
199
|
+
if (!target?.line) return { key: 'open', params: {} };
|
|
200
|
+
return target.column
|
|
201
|
+
? { key: 'openAtLineColumn', params: { line: target.line, column: target.column } }
|
|
202
|
+
: { key: 'openAtLine', params: { line: target.line } };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* What `GET /api/file-content` said about a path: the editor's own answer to "can this be opened".
|
|
207
|
+
* @param {{ status: number }} answer
|
|
208
|
+
* @returns {{ ok: true } | { ok: false, reason: 'missing' | 'notText' | 'tooLarge' | 'outside' | 'failed' }}
|
|
209
|
+
*/
|
|
210
|
+
export function describeProbe({ status } = {}) {
|
|
211
|
+
if (status === 200) return { ok: true };
|
|
212
|
+
if (status === 415) return { ok: false, reason: 'notText' };
|
|
213
|
+
if (status === 413) return { ok: false, reason: 'tooLarge' };
|
|
214
|
+
if (status === 403) return { ok: false, reason: 'outside' };
|
|
215
|
+
if (status === 400 || status === 404) return { ok: false, reason: 'missing' };
|
|
216
|
+
return { ok: false, reason: 'failed' };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// A file that is there but cannot be edited says more than one that is not there at all.
|
|
220
|
+
const TELLING = ['failed', 'outside', 'missing', 'tooLarge', 'notText'];
|
|
221
|
+
|
|
222
|
+
/** Of two reasons a path was not opened, the one worth telling the user. */
|
|
223
|
+
export function moreTelling(a, b) {
|
|
224
|
+
return TELLING.indexOf(b) > TELLING.indexOf(a) ? b : a;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** A `FILE_LINK_COPY` sentence with its `{params}` filled in. */
|
|
228
|
+
export function fillFileLinkCopy(key, params = {}) {
|
|
229
|
+
return String(FILE_LINK_COPY[key] ?? key).replace(/\{(\w+)\}/g, (whole, name) => (params[name] != null ? params[name] : whole));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── The click ──
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* What a link points at, as the agent wrote it.
|
|
236
|
+
* @param {Element} anchor
|
|
237
|
+
* @param {{ textLinks?: string }} [options] `textLinks`: the selector of the panel's own path links, the ones
|
|
238
|
+
* it makes out of a path in plain text (their text is the target, their `href` is "#")
|
|
239
|
+
* @returns {{ raw: string, text: boolean } | null}
|
|
240
|
+
*/
|
|
241
|
+
export function readFileLink(anchor, { textLinks = '' } = {}) {
|
|
242
|
+
const kept = anchor.getAttribute('data-file-target');
|
|
243
|
+
if (kept) return { raw: kept, text: false };
|
|
244
|
+
const href = anchor.getAttribute('href');
|
|
245
|
+
if (textLinks && anchor.matches(textLinks) && (!href || href === '#')) return { raw: anchor.textContent || '', text: true };
|
|
246
|
+
return href ? { raw: href, text: false } : null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The click and hover handlers of the links in a panel's messages.
|
|
251
|
+
* @param {object} page what the handlers need from the page
|
|
252
|
+
* @param {(event: string, data: object) => void} page.emit the bus (`open-file-editor`)
|
|
253
|
+
* @param {(path: string) => Promise<number>} page.probe the status GET /api/file-content answers for a path
|
|
254
|
+
* @param {() => Promise<string>} page.home the user's home folder ('' when it cannot be told)
|
|
255
|
+
* @param {() => string} page.origin the page's origin
|
|
256
|
+
* @param {(key: string, params?: object) => string} page.say a `fileLinks.*` sentence
|
|
257
|
+
* @param {(message: string) => void} page.notify shows a short message
|
|
258
|
+
* @param {(anchor: Element) => void} page.follow goes where a link of this app leads, as the browser would
|
|
259
|
+
*/
|
|
260
|
+
export function createFileLinkHandlers(page) {
|
|
261
|
+
const whereOf = (anchor, options) => {
|
|
262
|
+
let cwd = '';
|
|
263
|
+
try { cwd = options.cwd?.(anchor) || ''; } catch { cwd = ''; }
|
|
264
|
+
return { cwd, origin: page.origin() };
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
async function openOrFallBack(anchor, link, where, target, options) {
|
|
268
|
+
let found = target;
|
|
269
|
+
if (found.needsHome) found = parseFileLinkTarget(link.raw, { ...where, home: await page.home() });
|
|
270
|
+
|
|
271
|
+
let reason = null;
|
|
272
|
+
for (const path of found?.candidates || []) {
|
|
273
|
+
let answer;
|
|
274
|
+
try { answer = describeProbe({ status: await page.probe(path) }); } catch { answer = { ok: false, reason: 'failed' }; }
|
|
275
|
+
if (answer.ok) {
|
|
276
|
+
page.emit('open-file-editor', { filePath: path, line: found.line || undefined, column: found.column || undefined });
|
|
277
|
+
return 'opened';
|
|
278
|
+
}
|
|
279
|
+
reason = reason ? moreTelling(reason, answer.reason) : answer.reason;
|
|
280
|
+
}
|
|
281
|
+
reason ||= 'missing'; // nothing to try: `~` with no home folder to read it from
|
|
282
|
+
|
|
283
|
+
// Not a file the editor opens. The link keeps what it did before, or the reason is said.
|
|
284
|
+
if (link.text && typeof options.copyPath === 'function') {
|
|
285
|
+
try { await options.copyPath(anchor, withoutPlace(link.raw)); return 'copied'; } catch { /* could not copy: say why it did not open */ }
|
|
286
|
+
}
|
|
287
|
+
if (target.sameOrigin) { page.follow(anchor); return 'followed'; }
|
|
288
|
+
(options.notify || page.notify)(page.say(reason, { name: fileLinkName(found?.path || withoutPlace(link.raw)) }));
|
|
289
|
+
return reason;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Take a click on a link in an agent's message when the link points at a file.
|
|
294
|
+
* @param {MouseEvent} event
|
|
295
|
+
* @param {object} [options]
|
|
296
|
+
* @param {Element} [options.within] only links inside this element are taken
|
|
297
|
+
* @param {(anchor: Element) => string} [options.cwd] the working directory of the tab or session the link's
|
|
298
|
+
* message belongs to: a relative path is a path in it
|
|
299
|
+
* @param {string} [options.textLinks] selector of the panel's own path links (see readFileLink)
|
|
300
|
+
* @param {(anchor: Element, path: string) => any} [options.copyPath] what a click on one of those did
|
|
301
|
+
* before: it runs when the path is not a file the editor opens
|
|
302
|
+
* @param {(message: string) => void} [options.notify] shows the reason a link was not opened
|
|
303
|
+
* @returns {Promise<string> | null} null when the click is not this handler's (the browser follows the
|
|
304
|
+
* link as before); else the click is taken and the promise says how it ended: 'opened', 'copied',
|
|
305
|
+
* 'followed', or the reason the user was told
|
|
306
|
+
*/
|
|
307
|
+
function handleClick(event, options = {}) {
|
|
308
|
+
if (event.defaultPrevented || event.button > 0) return null;
|
|
309
|
+
const anchor = event.target?.closest?.('a');
|
|
310
|
+
if (!anchor || (options.within && !options.within.contains(anchor))) return null;
|
|
311
|
+
if (anchor.hasAttribute?.('download')) return null; // a file the app itself offers
|
|
312
|
+
const link = readFileLink(anchor, options);
|
|
313
|
+
if (!link) return null;
|
|
314
|
+
const where = whereOf(anchor, options);
|
|
315
|
+
const target = parseFileLinkTarget(link.raw, where);
|
|
316
|
+
if (!target) {
|
|
317
|
+
if (!link.text || typeof options.copyPath !== 'function') return null;
|
|
318
|
+
event.preventDefault();
|
|
319
|
+
try { options.copyPath(anchor, withoutPlace(link.raw)); } catch { /* nothing to copy to */ }
|
|
320
|
+
return Promise.resolve('copied');
|
|
321
|
+
}
|
|
322
|
+
event.preventDefault();
|
|
323
|
+
return openOrFallBack(anchor, link, where, target, options);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** A link the editor may open says so when it is pointed at, unless it already says something. */
|
|
327
|
+
function label(event, options = {}) {
|
|
328
|
+
const anchor = event.target?.closest?.('a');
|
|
329
|
+
if (!anchor || anchor.title || (options.within && !options.within.contains(anchor))) return;
|
|
330
|
+
const link = readFileLink(anchor, options);
|
|
331
|
+
if (!link || link.text) return;
|
|
332
|
+
const target = parseFileLinkTarget(link.raw, whereOf(anchor, options));
|
|
333
|
+
if (!target || target.sameOrigin) return;
|
|
334
|
+
const { key, params } = fileLinkTitle(target);
|
|
335
|
+
anchor.title = page.say(key, params);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return { handleClick, label };
|
|
339
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun — Greeting: the card every surface mounts
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// One renderer for the greeting: the Assistant and the Claude Code, Codex and
|
|
6
|
+
// OpenCode panels mount it inside the empty state they already have, Settings
|
|
7
|
+
// shows the same card as its preview. greeting-view.js decides the words; this
|
|
8
|
+
// file asks the server (`POST /api/greeting/open`) and draws.
|
|
9
|
+
//
|
|
10
|
+
// Everything is text (never markup): a greeting is built from records and may
|
|
11
|
+
// have been written by a model. A server older than this page (404) or an
|
|
12
|
+
// engine that did not start (503) simply shows nothing, and is asked again a
|
|
13
|
+
// minute later at the earliest.
|
|
14
|
+
|
|
15
|
+
import { greetingView, readGreetingResponse } from './greeting-view.js';
|
|
16
|
+
|
|
17
|
+
const STYLE_ID = 'sb-greet-styles';
|
|
18
|
+
const OPEN_URL = '/api/greeting/open';
|
|
19
|
+
const RETRY_MS = 60_000;
|
|
20
|
+
const TIMEOUT_MS = 4000;
|
|
21
|
+
|
|
22
|
+
// The app's own tokens (styles.css), so the card reads like the panel it sits in; a panel with
|
|
23
|
+
// tokens of its own maps them here.
|
|
24
|
+
const STYLES = `
|
|
25
|
+
.sb-greet { --sbg-text: var(--t-bright, rgba(255,255,255,0.9)); --sbg-2: var(--t-secondary, rgba(255,255,255,0.5)); --sbg-3: var(--t-muted, rgba(255,255,255,0.3)); --sbg-line: var(--b-subtle, rgba(255,255,255,0.06)); --sbg-fill: var(--s-subtle, rgba(255,255,255,0.04));
|
|
26
|
+
display: flex; flex-direction: column; align-items: center; gap: 4px; width: 100%; max-width: 440px; margin: 6px auto 0; padding: 0 12px; box-sizing: border-box; text-align: center; pointer-events: auto; }
|
|
27
|
+
.asst-empty .sb-greet { --sbg-text: var(--asst-text, var(--t-bright)); --sbg-2: var(--asst-text-2, var(--t-secondary)); --sbg-3: var(--asst-text-3, var(--t-muted)); margin-top: 0; }
|
|
28
|
+
.sb-greet[hidden] { display: none; }
|
|
29
|
+
.sb-greet-when { font-size: 12px; line-height: 1.45; color: var(--sbg-3); }
|
|
30
|
+
.sb-greet-text { margin: 0; font-size: 13px; line-height: 1.5; font-weight: 400; color: var(--sbg-text); overflow-wrap: anywhere; text-wrap: pretty; user-select: text; }
|
|
31
|
+
.sb-greet-more { appearance: none; display: inline-flex; align-items: center; gap: 4px; margin-top: 2px; padding: 2px 6px; border: 0; border-radius: var(--r-small, 6px); background: none; color: var(--sbg-3); font: inherit; font-size: 12px; line-height: 1.45; cursor: pointer; }
|
|
32
|
+
.sb-greet-more:is(:hover, :focus-visible) { color: var(--sbg-2); background: var(--sbg-fill); }
|
|
33
|
+
.sb-greet-more:focus-visible { outline: 1px solid var(--b-visible, rgba(255,255,255,0.22)); outline-offset: 1px; }
|
|
34
|
+
.sb-greet-chev { display: inline-block; transition: transform .12s ease; }
|
|
35
|
+
.sb-greet-more[aria-expanded="true"] .sb-greet-chev { transform: rotate(90deg); }
|
|
36
|
+
.sb-greet-basis { display: flex; flex-direction: column; gap: 4px; width: 100%; box-sizing: border-box; margin-top: 2px; padding: 8px 10px; border: 1px solid var(--sbg-line); border-radius: var(--r-card, 8px); background: var(--sbg-fill); text-align: left; }
|
|
37
|
+
.sb-greet-basis[hidden] { display: none; }
|
|
38
|
+
.sb-greet-row { display: grid; grid-template-columns: minmax(84px, 30%) minmax(0, 1fr) auto; gap: 8px; align-items: baseline; font-size: 12px; line-height: 1.45; }
|
|
39
|
+
.sb-greet-kind { color: var(--sbg-3); }
|
|
40
|
+
.sb-greet-label { color: var(--sbg-2); overflow-wrap: anywhere; user-select: text; }
|
|
41
|
+
.sb-greet-link { appearance: none; padding: 0 2px; border: 0; background: none; color: var(--sbg-3); font-family: var(--ff-mono, ui-monospace, 'JetBrains Mono', monospace); font-size: 11px; text-decoration: underline dotted; text-underline-offset: 2px; cursor: pointer; }
|
|
42
|
+
.sb-greet-link:is(:hover, :focus-visible) { color: var(--sbg-text); }
|
|
43
|
+
.sb-greet-cost { margin-top: 2px; padding-top: 6px; border-top: 1px solid var(--sbg-line); font-size: 12px; line-height: 1.45; color: var(--sbg-3); }
|
|
44
|
+
@media (prefers-reduced-motion: reduce) { .sb-greet-chev { transition: none; } }
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
let uid = 0;
|
|
48
|
+
const cache = new Map(); // `${surface}:${session}` → the greeting this conversation opened with (or null)
|
|
49
|
+
let unavailableUntil = 0; // an older server or an engine that did not start: not asked again before this
|
|
50
|
+
|
|
51
|
+
/** The card's styles, once per document. */
|
|
52
|
+
export function ensureGreetingStyles(doc = globalThis.document) {
|
|
53
|
+
if (!doc?.head || doc.getElementById(STYLE_ID)) return;
|
|
54
|
+
const style = doc.createElement('style');
|
|
55
|
+
style.id = STYLE_ID;
|
|
56
|
+
style.textContent = STYLES;
|
|
57
|
+
doc.head.appendChild(style);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function node(doc, tag, className, text) {
|
|
61
|
+
const el = doc.createElement(tag);
|
|
62
|
+
if (className) el.className = className;
|
|
63
|
+
if (text != null) el.textContent = text;
|
|
64
|
+
return el;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Draw a greeting inside `host` (a panel's empty state), replacing one already there.
|
|
69
|
+
* → the card element, or null when there is nothing to draw.
|
|
70
|
+
* opts: { t, now, onOpenMemory(id), open } (`open`: the disclosure starts open, Settings' preview)
|
|
71
|
+
*/
|
|
72
|
+
export function renderGreeting(host, greeting, { t = null, now = Date.now(), onOpenMemory = null, open = false } = {}) {
|
|
73
|
+
const doc = host?.ownerDocument || globalThis.document;
|
|
74
|
+
if (!host || !doc) return null;
|
|
75
|
+
for (const old of host.querySelectorAll(':scope > .sb-greet')) old.remove();
|
|
76
|
+
const view = greetingView(greeting, { now, t });
|
|
77
|
+
if (!view) return null;
|
|
78
|
+
ensureGreetingStyles(doc);
|
|
79
|
+
const card = node(doc, 'div', 'sb-greet');
|
|
80
|
+
card.setAttribute('role', 'note');
|
|
81
|
+
card.setAttribute('aria-label', view.label);
|
|
82
|
+
if (view.id) card.dataset.greetingId = view.id;
|
|
83
|
+
if (view.when) card.appendChild(node(doc, 'div', 'sb-greet-when', view.when));
|
|
84
|
+
card.appendChild(node(doc, 'p', 'sb-greet-text', view.text));
|
|
85
|
+
|
|
86
|
+
const basisId = `sb-greet-basis-${++uid}`;
|
|
87
|
+
const more = node(doc, 'button', 'sb-greet-more');
|
|
88
|
+
more.type = 'button';
|
|
89
|
+
more.setAttribute('aria-controls', basisId);
|
|
90
|
+
more.appendChild(node(doc, 'span', 'sb-greet-more-text', view.toggle));
|
|
91
|
+
const chev = node(doc, 'span', 'sb-greet-chev', '›');
|
|
92
|
+
chev.setAttribute('aria-hidden', 'true');
|
|
93
|
+
more.appendChild(chev);
|
|
94
|
+
const basis = node(doc, 'div', 'sb-greet-basis');
|
|
95
|
+
basis.id = basisId;
|
|
96
|
+
for (const row of view.rows) {
|
|
97
|
+
const line = node(doc, 'div', 'sb-greet-row');
|
|
98
|
+
line.dataset.kind = row.kind;
|
|
99
|
+
line.appendChild(node(doc, 'span', 'sb-greet-kind', row.kindLabel));
|
|
100
|
+
line.appendChild(node(doc, 'span', 'sb-greet-label', row.label));
|
|
101
|
+
if (row.id && typeof onOpenMemory === 'function') {
|
|
102
|
+
const link = node(doc, 'button', 'sb-greet-link', row.id.slice(0, 8));
|
|
103
|
+
link.type = 'button';
|
|
104
|
+
link.title = `${row.openLabel} ${row.id}`;
|
|
105
|
+
link.setAttribute('aria-label', link.title);
|
|
106
|
+
link.addEventListener('click', (event) => { event.stopPropagation(); try { onOpenMemory(row.id); } catch { /* the host decides */ } });
|
|
107
|
+
line.appendChild(link);
|
|
108
|
+
}
|
|
109
|
+
basis.appendChild(line);
|
|
110
|
+
}
|
|
111
|
+
basis.appendChild(node(doc, 'div', 'sb-greet-cost', view.cost));
|
|
112
|
+
const setOpen = (value) => { more.setAttribute('aria-expanded', value ? 'true' : 'false'); basis.hidden = !value; };
|
|
113
|
+
more.addEventListener('click', (event) => { event.stopPropagation(); setOpen(more.getAttribute('aria-expanded') !== 'true'); });
|
|
114
|
+
setOpen(open === true);
|
|
115
|
+
card.appendChild(more);
|
|
116
|
+
card.appendChild(basis);
|
|
117
|
+
host.appendChild(card);
|
|
118
|
+
return card;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Remove the card from a host. */
|
|
122
|
+
export function clearGreeting(host) {
|
|
123
|
+
if (!host) return;
|
|
124
|
+
for (const old of host.querySelectorAll(':scope > .sb-greet')) old.remove();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Ask whether this fresh conversation is greeted. One request per conversation: the answer is
|
|
129
|
+
* kept, so a redraw of the empty state costs nothing. → the greeting, or null.
|
|
130
|
+
* `session`: the surface's own name for the conversation (the server gives the same one the same answer).
|
|
131
|
+
*/
|
|
132
|
+
export async function fetchGreeting({ surface, cwd = null, project = null, session = null, fetchImpl = globalThis.fetch, now = Date.now } = {}) {
|
|
133
|
+
if (!surface || typeof fetchImpl !== 'function') return null;
|
|
134
|
+
const key = session ? `${surface}:${session}` : null;
|
|
135
|
+
if (key && cache.has(key)) return cache.get(key);
|
|
136
|
+
if (now() < unavailableUntil) return null;
|
|
137
|
+
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
|
138
|
+
const timer = controller ? setTimeout(() => controller.abort(), TIMEOUT_MS) : null;
|
|
139
|
+
try {
|
|
140
|
+
const response = await fetchImpl(OPEN_URL, {
|
|
141
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, signal: controller?.signal,
|
|
142
|
+
body: JSON.stringify({ surface, ...(project ? { project } : {}), ...(cwd ? { cwd } : {}), ...(session ? { session } : {}) }),
|
|
143
|
+
});
|
|
144
|
+
let body = null;
|
|
145
|
+
try { body = await response.json(); } catch { body = null; }
|
|
146
|
+
const answer = readGreetingResponse(response.status, body);
|
|
147
|
+
if (answer.state === 'old-server' || answer.state === 'failed') { unavailableUntil = now() + RETRY_MS; return null; }
|
|
148
|
+
if (answer.state === 'unreadable') return null;
|
|
149
|
+
if (key) { cache.set(key, answer.greeting); while (cache.size > 200) cache.delete(cache.keys().next().value); }
|
|
150
|
+
return answer.greeting;
|
|
151
|
+
} catch { return null; } finally { if (timer) clearTimeout(timer); }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** The greeting a conversation opened with, if it was asked already (no request). */
|
|
155
|
+
export function knownGreeting(surface, session) {
|
|
156
|
+
return session ? cache.get(`${surface}:${session}`) || null : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The conversation sent its first message: its greeting is not drawn again. */
|
|
160
|
+
export function forgetGreeting(surface, session) {
|
|
161
|
+
if (session) cache.set(`${surface}:${session}`, null);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Ask and draw. `host`: the empty state element; `isCurrent()`: false when the conversation is no
|
|
166
|
+
* longer the one on screen (the answer arrived late). → the greeting drawn, or null.
|
|
167
|
+
*/
|
|
168
|
+
export async function mountGreeting(host, { surface, cwd = null, project = null, session = null, t = null, onOpenMemory = null, isCurrent = null, fetchImpl = globalThis.fetch } = {}) {
|
|
169
|
+
if (!host) return null;
|
|
170
|
+
const greeting = await fetchGreeting({ surface, cwd, project, session, fetchImpl });
|
|
171
|
+
// (A host that is not in the document yet is fine: a panel may draw a tab before it shows it.)
|
|
172
|
+
if (!greeting) return null;
|
|
173
|
+
if (typeof isCurrent === 'function') { try { if (isCurrent() === false) return greeting; } catch { return greeting; } }
|
|
174
|
+
renderGreeting(host, greeting, { t, onOpenMemory });
|
|
175
|
+
return greeting;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export const _test = { reset() { cache.clear(); unavailableUntil = 0; } };
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun — Greeting: what a surface shows (DOM-free)
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// The one greeting every surface shows (the Assistant, the Claude Code, Codex
|
|
6
|
+
// and OpenCode panels, Settings' preview): the sentence SynaBun wrote, how long
|
|
7
|
+
// the person was away, and a disclosure of what it was based on and what it
|
|
8
|
+
// cost. This module decides every word; greeting-card.js draws it.
|
|
9
|
+
//
|
|
10
|
+
// GREETING_COPY is the English; `greeting.*` in i18n/*.json must equal it
|
|
11
|
+
// (tests/greeting-view.test.mjs). Elapsed time is computed here, at display:
|
|
12
|
+
// the sentence itself never says how long it has been, so one written hours
|
|
13
|
+
// ago is still true.
|
|
14
|
+
|
|
15
|
+
export const GREETING_COPY = Object.freeze({
|
|
16
|
+
label: 'Greeting from SynaBun',
|
|
17
|
+
back: 'Back after {time}',
|
|
18
|
+
'time.minute': '1 minute',
|
|
19
|
+
'time.minutes': '{n} minutes',
|
|
20
|
+
'time.hour': '1 hour',
|
|
21
|
+
'time.hours': '{n} hours',
|
|
22
|
+
'time.day': '1 day',
|
|
23
|
+
'time.days': '{n} days',
|
|
24
|
+
'time.week': '1 week',
|
|
25
|
+
'time.weeks': '{n} weeks',
|
|
26
|
+
'time.month': '1 month',
|
|
27
|
+
'time.months': '{n} months',
|
|
28
|
+
'basis.one': 'Based on 1 record',
|
|
29
|
+
'basis.many': 'Based on {n} records',
|
|
30
|
+
'basis.none': 'What this is based on',
|
|
31
|
+
'kind.thread': 'Last conversation',
|
|
32
|
+
'kind.session': 'Last session',
|
|
33
|
+
'kind.open': 'Open',
|
|
34
|
+
'kind.memory': 'Noted',
|
|
35
|
+
'kind.run': 'Run',
|
|
36
|
+
'kind.schedule': 'Schedule',
|
|
37
|
+
'kind.elsewhere': 'Elsewhere',
|
|
38
|
+
'kind.git': 'Working tree',
|
|
39
|
+
'kind.owner': 'About you',
|
|
40
|
+
'agent.assistant': 'Assistant',
|
|
41
|
+
'agent.claude-code': 'Claude Code',
|
|
42
|
+
'agent.codex': 'Codex',
|
|
43
|
+
'agent.opencode': 'OpenCode',
|
|
44
|
+
open: 'Open memory',
|
|
45
|
+
'source.writer': 'Written ahead by {model}',
|
|
46
|
+
'source.fallback': 'Put together from the records, no model',
|
|
47
|
+
'why.not-prepared': 'nothing was written ahead yet',
|
|
48
|
+
'why.out-of-date': 'what was written ahead was older than the news',
|
|
49
|
+
'why.no-writer': 'no Claude sign-in to write with',
|
|
50
|
+
'why.daily-cap': 'the daily limit of writer calls was reached',
|
|
51
|
+
'why.other': 'the writer gave no usable sentence',
|
|
52
|
+
tokens: '{n} tokens',
|
|
53
|
+
prepared: 'written {time} ago',
|
|
54
|
+
free: 'no tokens spent',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const num = (value) => (Number.isFinite(Number(value)) && Number(value) > 0 ? Math.round(Number(value)) : 0);
|
|
58
|
+
|
|
59
|
+
/** A string of the greeting, through the app's translator when it has one. */
|
|
60
|
+
export function greetingText(key, params = null, t = null) {
|
|
61
|
+
const full = `greeting.${key}`;
|
|
62
|
+
let value;
|
|
63
|
+
try { value = typeof t === 'function' ? t(full, params || undefined) : undefined; } catch { value = undefined; }
|
|
64
|
+
if (typeof value === 'string' && value && value !== full) return value;
|
|
65
|
+
const fallback = GREETING_COPY[key];
|
|
66
|
+
if (typeof fallback !== 'string') return key;
|
|
67
|
+
return params ? fallback.replace(/\{(\w+)\}/g, (_, name) => (params[name] != null ? String(params[name]) : `{${name}}`)) : fallback;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** "40 minutes" · "3 hours" · "2 days" · "3 weeks" · "4 months", in the app's language. '' for no time. */
|
|
71
|
+
export function formatAway(ms, t = null) {
|
|
72
|
+
if (!Number.isFinite(Number(ms)) || Number(ms) < 0) return '';
|
|
73
|
+
const minutes = Math.round(Number(ms) / 60_000);
|
|
74
|
+
const unit = (one, many, n) => (n === 1 ? greetingText(`time.${one}`, null, t) : greetingText(`time.${many}`, { n }, t));
|
|
75
|
+
if (minutes < 60) return unit('minute', 'minutes', Math.max(1, minutes));
|
|
76
|
+
const hours = Math.round(minutes / 60);
|
|
77
|
+
if (hours < 36) return unit('hour', 'hours', hours);
|
|
78
|
+
const days = Math.round(hours / 24);
|
|
79
|
+
if (days < 21) return unit('day', 'days', days);
|
|
80
|
+
if (days < 75) return unit('week', 'weeks', Math.round(days / 7));
|
|
81
|
+
return unit('month', 'months', Math.round(days / 30));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** "$0.0006" · "<$0.0001" · '' when unknown or nothing. */
|
|
85
|
+
export function formatGreetingCost(value) {
|
|
86
|
+
const usd = Number(value);
|
|
87
|
+
if (!Number.isFinite(usd) || usd <= 0) return '';
|
|
88
|
+
if (usd < 0.0001) return '<$0.0001';
|
|
89
|
+
return `$${usd < 0.01 ? usd.toFixed(4) : usd.toFixed(2)}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Everything a surface draws for one greeting (the engine's view: lib/greeting/engine.js).
|
|
94
|
+
* → { id, label, when, text, toggle, rows:[{ kind, kindLabel, label, id, openLabel }], cost } | null
|
|
95
|
+
* `when` is '' when the person was not away (a preview): the caller hides the line.
|
|
96
|
+
*/
|
|
97
|
+
export function greetingView(greeting, { now = Date.now(), t = null } = {}) {
|
|
98
|
+
const text = typeof greeting?.text === 'string' ? greeting.text.trim() : '';
|
|
99
|
+
if (!text) return null;
|
|
100
|
+
const since = Date.parse(greeting.awaySince ?? '');
|
|
101
|
+
const away = Number.isFinite(since) ? formatAway(Math.max(0, now - since), t) : '';
|
|
102
|
+
const basis = Array.isArray(greeting.basis) ? greeting.basis : [];
|
|
103
|
+
const rows = basis.filter((row) => row && typeof row.label === 'string' && row.label.trim()).map((row) => {
|
|
104
|
+
const kind = GREETING_COPY[`kind.${row.kind}`] ? row.kind : 'memory';
|
|
105
|
+
const agent = (kind === 'thread' || kind === 'session') && row.agent && GREETING_COPY[`agent.${row.agent}`] ? greetingText(`agent.${row.agent}`, null, t) : '';
|
|
106
|
+
return {
|
|
107
|
+
kind, kindLabel: agent ? `${greetingText(`kind.${kind}`, null, t)} · ${agent}` : greetingText(`kind.${kind}`, null, t),
|
|
108
|
+
label: row.label.trim(), id: typeof row.id === 'string' && row.id ? row.id : null,
|
|
109
|
+
openLabel: greetingText('open', null, t),
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
const cost = [];
|
|
113
|
+
if (greeting.source === 'writer') {
|
|
114
|
+
cost.push(greetingText('source.writer', { model: greeting.model || 'Claude' }, t));
|
|
115
|
+
const tokens = greeting.tokens && typeof greeting.tokens === 'object' ? num(greeting.tokens.input) + num(greeting.tokens.cacheWrite) + num(greeting.tokens.cacheRead) + num(greeting.tokens.output) : 0;
|
|
116
|
+
if (tokens) cost.push(greetingText('tokens', { n: tokens }, t));
|
|
117
|
+
const usd = formatGreetingCost(greeting.costUsd);
|
|
118
|
+
if (usd) cost.push(usd);
|
|
119
|
+
const prepared = Date.parse(greeting.preparedAt ?? '');
|
|
120
|
+
if (Number.isFinite(prepared) && now - prepared >= 60_000) cost.push(greetingText('prepared', { time: formatAway(now - prepared, t) }, t));
|
|
121
|
+
} else {
|
|
122
|
+
const why = greeting.error && GREETING_COPY[`why.${greeting.error}`] ? greeting.error : (greeting.error ? 'other' : null);
|
|
123
|
+
cost.push(why ? `${greetingText('source.fallback', null, t)}: ${greetingText(`why.${why}`, null, t)}` : greetingText('source.fallback', null, t));
|
|
124
|
+
cost.push(greetingText('free', null, t));
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
id: typeof greeting.id === 'string' ? greeting.id : null,
|
|
128
|
+
label: greetingText('label', null, t),
|
|
129
|
+
when: away ? greetingText('back', { time: away }, t) : '',
|
|
130
|
+
text,
|
|
131
|
+
toggle: rows.length === 0 ? greetingText('basis.none', null, t) : rows.length === 1 ? greetingText('basis.one', null, t) : greetingText('basis.many', { n: rows.length }, t),
|
|
132
|
+
rows,
|
|
133
|
+
cost: cost.join(' · '),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* What /api/greeting/open answered. → { state, greeting, reason }
|
|
139
|
+
* state: 'ok' (a greeting) · 'none' (nothing to say now) · 'old-server' (404: the page is newer than the
|
|
140
|
+
* server) · 'failed' (503: the engine did not start) · 'unreadable'
|
|
141
|
+
*/
|
|
142
|
+
export function readGreetingResponse(status, body) {
|
|
143
|
+
if (status === 404) return { state: 'old-server', greeting: null, reason: null };
|
|
144
|
+
if (status === 503) return { state: 'failed', greeting: null, reason: typeof body?.reason === 'string' ? body.reason : null };
|
|
145
|
+
if (status !== 200 || !body || body.ok !== true) return { state: 'unreadable', greeting: null, reason: null };
|
|
146
|
+
const greeting = body.greeting && typeof body.greeting === 'object' && typeof body.greeting.text === 'string' && body.greeting.text.trim() ? body.greeting : null;
|
|
147
|
+
return { state: greeting ? 'ok' : 'none', greeting, reason: typeof body.reason === 'string' ? body.reason : null };
|
|
148
|
+
}
|