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
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from './cdx-plan.js';
|
|
14
14
|
import { fetchBlobNamespace, putBlob, deleteBlob, flushBlobs } from '../blob-store.js';
|
|
15
15
|
import { state as appState, on as appOn } from '../state.js';
|
|
16
|
+
import { forgetGreeting, mountGreeting } from '../greeting/greeting-card.js';
|
|
16
17
|
import { fetchProjects, searchSessions } from '../api.js';
|
|
17
18
|
import {
|
|
18
19
|
getCodexModelName,
|
|
@@ -24,6 +25,8 @@ import { syncSidepanelLayout, focusSidepanel, isHostedSessionFocused } from '../
|
|
|
24
25
|
import { notify, NOTIF_TYPE } from '../ui-notifications.js';
|
|
25
26
|
import { createStickyScrollController } from '../ui-scroll-follow.js';
|
|
26
27
|
import { requestGeneratedSessionTitle } from '../session-title.js';
|
|
28
|
+
import { fileLinkText } from '../ui-file-links.js';
|
|
29
|
+
import { WORKER_END_CLASS, backToAssistant, buildWorkerEndCard, endWorkerRunOnClose, forgetWorkerEnd } from '../ui-worker-end.js';
|
|
27
30
|
import {
|
|
28
31
|
CODEX_COMMANDS, CODEX_COMMAND_BY_NAME, parseCodexCommand, codexCommandHints,
|
|
29
32
|
capabilityAvailability, latestCompletedCodexText, configValue, configRestriction,
|
|
@@ -467,6 +470,8 @@ function buildWsCallbacks(tab, options = {}) {
|
|
|
467
470
|
clearTranscript();
|
|
468
471
|
}
|
|
469
472
|
restoreServerRequests(heldRequests);
|
|
473
|
+
// The history rebuilt the transcript: a dispatched worker's end card goes back at its end.
|
|
474
|
+
placeWorkerEnd(tab);
|
|
470
475
|
if (options.historyOnly) {
|
|
471
476
|
tab._automationHydrated = true;
|
|
472
477
|
closeAutomationHistorySocket(tab);
|
|
@@ -1247,6 +1252,7 @@ function writeThreadSnapshot(tab, { force = false } = {}) {
|
|
|
1247
1252
|
const accountId = String(tab.accountId || 'default');
|
|
1248
1253
|
const snapshotNodes = [...messagesEl.children].filter((node) => (
|
|
1249
1254
|
!node.classList?.contains('cxp-empty') && !node.classList?.contains('cxp-thinking')
|
|
1255
|
+
&& !node.classList?.contains(WORKER_END_CLASS)
|
|
1250
1256
|
));
|
|
1251
1257
|
const ownsEveryNode = codexTranscriptNodesHaveOwnership(snapshotNodes, { threadId, accountId });
|
|
1252
1258
|
if (!ownsEveryNode) {
|
|
@@ -1257,6 +1263,8 @@ function writeThreadSnapshot(tab, { force = false } = {}) {
|
|
|
1257
1263
|
}
|
|
1258
1264
|
const snapshotEl = messagesEl.cloneNode(true);
|
|
1259
1265
|
stripMcpAppRuntime(snapshotEl);
|
|
1266
|
+
// A worker's end card is drawn again from its run, never restored from markup.
|
|
1267
|
+
snapshotEl.querySelectorAll(`.${WORKER_END_CLASS}`).forEach((node) => node.remove());
|
|
1260
1268
|
const html = snapshotEl.innerHTML || '';
|
|
1261
1269
|
if (!html.trim()) return;
|
|
1262
1270
|
const ownedItemNodes = snapshotNodes.filter((node) => node.dataset?.itemId);
|
|
@@ -1506,6 +1514,11 @@ function storedAutoAccept() {
|
|
|
1506
1514
|
function releaseAutomationForManualUse(tab = _boundTab) {
|
|
1507
1515
|
const runId = detachTerminalCodexAutomation(tab, storedAutoAccept());
|
|
1508
1516
|
if (!runId) return false;
|
|
1517
|
+
// While the tab still shows the run's thread, the dock's Focus on the run raises it (cdx-panel.js).
|
|
1518
|
+
tab.releasedRunId = runId;
|
|
1519
|
+
forgetWorkerEnd(runId);
|
|
1520
|
+
tab._workerEnd = null;
|
|
1521
|
+
placeWorkerEnd(tab);
|
|
1509
1522
|
window.dispatchEvent(new CustomEvent('native-loop:dismissed', { detail: { runId } }));
|
|
1510
1523
|
if (isActiveTab(tab)) syncToolbarState();
|
|
1511
1524
|
saveTabs();
|
|
@@ -2843,6 +2856,7 @@ export function createTab(saved = {}, { autoSwitch = true } = {}) {
|
|
|
2843
2856
|
automationRunId,
|
|
2844
2857
|
automationActive: !!saved.automationActive,
|
|
2845
2858
|
automationOwnerId: saved.automationOwnerId || null,
|
|
2859
|
+
releasedRunId: automationRunId ? null : (saved.releasedRunId || null),
|
|
2846
2860
|
};
|
|
2847
2861
|
tab.messagesEl = createMessagesEl(tab);
|
|
2848
2862
|
tab.pillEl = createTrayPill(tab);
|
|
@@ -2870,15 +2884,18 @@ export function closeTab(idx, { detachAutomation = false } = {}) {
|
|
|
2870
2884
|
const tab = _tabs[idx];
|
|
2871
2885
|
teardownMcpApps(tab, 'Tab closed');
|
|
2872
2886
|
if (tab.automationRunId && !detachAutomation) {
|
|
2887
|
+
const runId = tab.automationRunId;
|
|
2873
2888
|
window.dispatchEvent(new CustomEvent('native-loop:dismissed', {
|
|
2874
|
-
detail: { runId
|
|
2889
|
+
detail: { runId },
|
|
2875
2890
|
}));
|
|
2876
|
-
|
|
2891
|
+
// A worker that finished its task is completed; any other run is stopped.
|
|
2892
|
+
endWorkerRunOnClose(runId, () => fetch('/api/loop/stop', {
|
|
2877
2893
|
method: 'POST',
|
|
2878
2894
|
headers: { 'Content-Type': 'application/json' },
|
|
2879
|
-
body: JSON.stringify({ runId
|
|
2880
|
-
}).catch(() => {});
|
|
2895
|
+
body: JSON.stringify({ runId }),
|
|
2896
|
+
}).catch(() => {}));
|
|
2881
2897
|
}
|
|
2898
|
+
if (tab.automationRunId) forgetWorkerEnd(tab.automationRunId);
|
|
2882
2899
|
wsDisconnectTab(tab, { releaseWriter: !tabHasActiveWriterWork(tab) });
|
|
2883
2900
|
tab.closed = true;
|
|
2884
2901
|
tab.titleRequestController?.abort();
|
|
@@ -2923,6 +2940,31 @@ export function closeTab(idx, { detachAutomation = false } = {}) {
|
|
|
2923
2940
|
saveTabs();
|
|
2924
2941
|
}
|
|
2925
2942
|
|
|
2943
|
+
/**
|
|
2944
|
+
* The end of a worker the Assistant dispatched (ui-worker-end.js), as
|
|
2945
|
+
* `tab._workerEnd` says it: one card, the last node of the transcript, put
|
|
2946
|
+
* back after a history render and never in a snapshot. Its button closes the
|
|
2947
|
+
* tab the way the X does (a warm worker that finished its task is completed,
|
|
2948
|
+
* one still at work is stopped; its record stays for the Assistant) and shows
|
|
2949
|
+
* the Assistant session that dispatched it.
|
|
2950
|
+
*/
|
|
2951
|
+
export function placeWorkerEnd(tab) {
|
|
2952
|
+
const messagesEl = tab?.messagesEl;
|
|
2953
|
+
if (!messagesEl || tab.closed) return;
|
|
2954
|
+
messagesEl.querySelectorAll(`:scope > .${WORKER_END_CLASS}`).forEach((node) => node.remove());
|
|
2955
|
+
const view = tab._workerEnd;
|
|
2956
|
+
if (!view) return;
|
|
2957
|
+
messagesEl.appendChild(buildWorkerEndCard(view, {
|
|
2958
|
+
classes: { card: 'cxp-worker-end', title: 'cxp-post-plan-header', detail: 'cxp-post-plan-note', button: 'cxp-request-btn' },
|
|
2959
|
+
onBack: () => {
|
|
2960
|
+
const idx = _tabs.indexOf(tab);
|
|
2961
|
+
if (idx >= 0) closeTab(idx);
|
|
2962
|
+
backToAssistant(view.assistantSessionId);
|
|
2963
|
+
},
|
|
2964
|
+
}));
|
|
2965
|
+
if (isActiveTab(tab)) scrollEnd(true);
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2926
2968
|
export function saveTabs() {
|
|
2927
2969
|
try {
|
|
2928
2970
|
commitBoundState();
|
|
@@ -2977,6 +3019,7 @@ export function saveTabs() {
|
|
|
2977
3019
|
automationRunId: tab.automationRunId || null,
|
|
2978
3020
|
automationActive: !!tab.automationActive,
|
|
2979
3021
|
automationOwnerId: tab.automationOwnerId || null,
|
|
3022
|
+
releasedRunId: tab.releasedRunId || null,
|
|
2980
3023
|
};
|
|
2981
3024
|
});
|
|
2982
3025
|
const localRunIds = new Set(localTabs.map((tab) => tab.automationRunId).filter(Boolean));
|
|
@@ -3097,6 +3140,34 @@ export function updateTrayPillRunning() {
|
|
|
3097
3140
|
}
|
|
3098
3141
|
|
|
3099
3142
|
|
|
3143
|
+
// ── Greeting (shared/greeting/, lib/greeting/) ──
|
|
3144
|
+
// What SynaBun says when a person comes back and opens a new thread: one or two sentences in the
|
|
3145
|
+
// empty state, written by SynaBun ahead of time (the thread's model never greets). Only for the
|
|
3146
|
+
// bound tab while it holds no thread and shows no automation run; the short wait lets a tab that
|
|
3147
|
+
// was made for a run, or is restoring a thread, say so first. Its id goes back with the first
|
|
3148
|
+
// prompt (`greetingId`), so the model is told once what was said.
|
|
3149
|
+
function offerGreetingSoon() {
|
|
3150
|
+
const tab = _boundTab;
|
|
3151
|
+
if (!tab) return;
|
|
3152
|
+
setTimeout(() => { try { offerGreeting(tab); } catch { /* a thread always opens */ } }, 300);
|
|
3153
|
+
}
|
|
3154
|
+
function offerGreeting(tab) {
|
|
3155
|
+
if (tab !== _boundTab || tab.closed || _threadId || _running || _startingThread || tab.automationActive || tab.automationRunId) return;
|
|
3156
|
+
const host = _messagesEl?.querySelector(':scope > .cxp-empty');
|
|
3157
|
+
if (!host || host.style.display === 'none') return;
|
|
3158
|
+
// "Restoring Codex thread…" / "Starting fresh Codex thread…": not an idle new thread.
|
|
3159
|
+
if (host.querySelector('.cxp-empty-status')?.textContent) return;
|
|
3160
|
+
const key = () => `${tab.id}.${tab.greetSeq || 0}`;
|
|
3161
|
+
const session = key();
|
|
3162
|
+
mountGreeting(host, {
|
|
3163
|
+
surface: 'codex-panel', session, cwd: _project || tab.project || '',
|
|
3164
|
+
onOpenMemory: (id) => { if (id) emit('memory:open', { id }); },
|
|
3165
|
+
isCurrent: () => tab === _boundTab && !_threadId && session === key() && !!_messagesEl?.contains(host),
|
|
3166
|
+
}).then((greeting) => {
|
|
3167
|
+
if (session === key()) tab.greeting = greeting ? { id: greeting.id, session } : null;
|
|
3168
|
+
}).catch(() => {});
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3100
3171
|
// ── Thread state (lines 4711-5000) ──
|
|
3101
3172
|
function hideEmpty() {
|
|
3102
3173
|
const empty = _messagesEl?.querySelector('.cxp-empty');
|
|
@@ -3125,6 +3196,7 @@ export function clearTranscript(emptyText = freshThreadText()) {
|
|
|
3125
3196
|
for (const child of [..._messagesEl.children]) retireMcpAppNode(child, () => child.remove());
|
|
3126
3197
|
_messagesEl.insertAdjacentHTML('afterbegin', `<div class="cxp-empty"><div class="cxp-empty-logo">${OPENAI_ICON}</div><span class="cxp-empty-name">Codex</span><span class="cxp-empty-status"></span></div>`);
|
|
3127
3198
|
document.dispatchEvent(new CustomEvent('cdx-empty-rebuilt'));
|
|
3199
|
+
if (!emptyText) offerGreetingSoon();
|
|
3128
3200
|
_items.clear();
|
|
3129
3201
|
_requestCards.clear();
|
|
3130
3202
|
_blockingServerRequests = new Map();
|
|
@@ -3285,6 +3357,8 @@ export function resetThreadState({ tone = 'ready', preserveStatus = false } = {}
|
|
|
3285
3357
|
clearPostCompactionState(_boundTab, { updateTranscript: false });
|
|
3286
3358
|
setSessionLabel('New session', { persist: false });
|
|
3287
3359
|
if (!preserveStatus) setStatus(_project ? 'Fresh thread ready' : 'Select a project', tone);
|
|
3360
|
+
// A new thread in the same tab is a new opening for the greeting.
|
|
3361
|
+
if (_boundTab) { _boundTab.greetSeq = (_boundTab.greetSeq || 0) + 1; _boundTab.greeting = null; }
|
|
3288
3362
|
clearTranscript();
|
|
3289
3363
|
syncInputEnabled();
|
|
3290
3364
|
syncSessionControls();
|
|
@@ -4954,6 +5028,14 @@ export function dispatchPrompt(prompt, { tab = activeTab(), images = null, paths
|
|
|
4954
5028
|
};
|
|
4955
5029
|
if (tab?.model) msg.model = tab.model;
|
|
4956
5030
|
if (tab?.effort && tab.effort !== 'off') msg.effort = tab.effort;
|
|
5031
|
+
// A person's own turn: the greeting engine keeps when they were last here. The greeting this
|
|
5032
|
+
// tab opened with goes back once, with the first prompt of its new thread.
|
|
5033
|
+
msg.human = true;
|
|
5034
|
+
if (tab?.greeting?.id) {
|
|
5035
|
+
if (msg.fresh) msg.greetingId = tab.greeting.id;
|
|
5036
|
+
forgetGreeting('codex-panel', tab.greeting.session);
|
|
5037
|
+
tab.greeting = null;
|
|
5038
|
+
}
|
|
4957
5039
|
if (imageList.length) {
|
|
4958
5040
|
msg.images = imageList;
|
|
4959
5041
|
}
|
|
@@ -7929,6 +8011,7 @@ export function initContextBridge() {
|
|
|
7929
8011
|
withRequestTab: withTab,
|
|
7930
8012
|
sendSocket,
|
|
7931
8013
|
emit,
|
|
8014
|
+
fileLinkText,
|
|
7932
8015
|
activeTab,
|
|
7933
8016
|
isActiveTab,
|
|
7934
8017
|
dispatchPrompt,
|
|
@@ -64,6 +64,7 @@ export const KEYS = {
|
|
|
64
64
|
WORKSPACES: 'neural-workspaces',
|
|
65
65
|
ACTIVE_WORKSPACE: 'neural-active-workspace', // sessionStorage
|
|
66
66
|
GRID_SNAP: 'neural-grid-snap',
|
|
67
|
+
TOPRIGHT_TOOLS_HIDDEN: 'neural-topright-tools-hidden', // the workspace tools toggle is off ('true')
|
|
67
68
|
CATEGORIES_VISIBLE: 'neural-categories-visible',
|
|
68
69
|
WHITEBOARD: 'neural-whiteboard',
|
|
69
70
|
WHITEBOARD_RULESET_ALERT_DISMISSED: 'neural-whiteboard-ruleset-alert-dismissed',
|
|
@@ -85,6 +86,10 @@ export const KEYS = {
|
|
|
85
86
|
FILE_EXPLORER_SORT: 'neural-file-explorer-sort',
|
|
86
87
|
FILE_EXPLORER_PROJECT: 'neural-file-explorer-project',
|
|
87
88
|
FILE_EXPLORER_FOLDER_COLORS: 'neural-file-explorer-folder-colors',
|
|
89
|
+
// Per project: the open folders, the selection, the scroll position, "changed only" (shared/explorer/fx-main.js).
|
|
90
|
+
FILE_EXPLORER_TREE: 'neural-file-explorer-tree',
|
|
91
|
+
// Code editor — open tabs per project { v, projects: { [path]: { at, active, tabs: [{ path, pinned, anchor, head, top, dirty }] } } }
|
|
92
|
+
EDITOR_TABS: 'neural-editor-tabs',
|
|
88
93
|
ACTIVE_SKIN: 'neural-active-skin',
|
|
89
94
|
COST_WIDGET_POS: 'neural-cost-widget-pos',
|
|
90
95
|
COST_WIDGET_COLLAPSED: 'neural-cost-widget-collapsed',
|
|
@@ -97,6 +102,7 @@ export const KEYS = {
|
|
|
97
102
|
ASSISTANT_DOCK_OPEN: 'neural-assistant-dock-open',
|
|
98
103
|
ASSISTANT_DOCK_SHOW_ALL:'neural-assistant-dock-show-all',
|
|
99
104
|
ASSISTANT_ROUTE_MODE: 'neural-assistant-route-mode', // last-used route mode (always-ask|ask-unsure|never)
|
|
105
|
+
ASSISTANT_TEST_DENSITY: 'neural-assistant-test-density', // last-used test density (none|light|standard|thorough)
|
|
100
106
|
// Assistant sidepanel — open tabs {v:1, activeIdx, tabs:[{sessionId,label,userRenamed}]}
|
|
101
107
|
ASSISTANT_PANEL_TABS: 'synabun-assistant-panel-tabs',
|
|
102
108
|
};
|
|
@@ -141,7 +147,6 @@ export const KEYBIND_META = {
|
|
|
141
147
|
'toggle-explorer': { label: 'Toggle Explorer', group: 'Navigation' },
|
|
142
148
|
'toggle-focus-mode': { label: 'Toggle Focus Mode', group: 'Navigation' },
|
|
143
149
|
'toggle-minimap': { label: 'Toggle Minimap', group: 'View' },
|
|
144
|
-
'toggle-toolbar': { label: 'Hide All Toolbars', group: 'View' },
|
|
145
150
|
'open-settings': { label: 'Open Settings', group: 'Settings' },
|
|
146
151
|
'open-keybinds': { label: 'Open Keybinds', group: 'Settings' },
|
|
147
152
|
'launch-assistant': { label: 'Assistant', group: 'Launch App', icon: 'assistant' },
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
import { pagerState } from './cp-restore.js';
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// (A worker's end card is drawn again from its run: ui-worker-end.js.)
|
|
18
|
+
const STALE_ROWS = '.thinking, .msg-permission-prompt, .post-plan-card, .worker-end-card';
|
|
18
19
|
const STALE_PROMPTS = '.cp-plan-approval-card.active-perm, .perm-card.active-perm, .cp-rewind-confirm';
|
|
19
20
|
const SELF_WIRED_CARDS = '.tool-card.cp-bash-card, .tool-card.cp-diff-card, .tool-card.cp-agent-card';
|
|
20
21
|
|
|
@@ -346,4 +346,19 @@ a.cp-chip:hover { text-decoration: underline; }
|
|
|
346
346
|
.cp-ctxpop-copy { padding: 0; border: none; background: none; font: inherit; line-height: 20px; cursor: pointer; }
|
|
347
347
|
.cp-ctxpop-copy:hover, .cp-ctxpop-copy.cp-ctxpop-copied { color: rgba(220,195,140,0.9); }
|
|
348
348
|
.cp-ctxpop-btn:focus-visible, .cp-ctxpop-link:focus-visible, .cp-ctxpop-copy:focus-visible { outline: 1px solid rgba(255,255,255,0.45); outline-offset: 2px; }
|
|
349
|
+
|
|
350
|
+
/* ═══ A dispatched worker's end (ui-worker-end.js) ═══
|
|
351
|
+
The post-plan card's frame, header and primary action, in the transcript column. The title says the state in
|
|
352
|
+
words; its colour is the panel's success / warning / error. */
|
|
353
|
+
.cp-messages .cp-worker-end { margin: 8px 14px 10px 48px; padding: 8px 10px; border: 1px solid rgba(255,255,255,0.07); border-radius: 8px; background: rgba(255,255,255,0.02); }
|
|
354
|
+
.cp-worker-end .post-plan-header { margin-bottom: 4px; }
|
|
355
|
+
.cp-worker-end[data-tone="ok"] .post-plan-header { color: #5fcf83; }
|
|
356
|
+
.cp-worker-end[data-tone="warn"] .post-plan-header { color: #e0a33e; }
|
|
357
|
+
.cp-worker-end[data-tone="err"] .post-plan-header { color: #ef7070; }
|
|
358
|
+
.cp-worker-end .worker-end-detail { font: 11.5px/1.55 Inter, sans-serif; color: var(--t-secondary); overflow-wrap: anywhere; }
|
|
359
|
+
.cp-worker-end .worker-end-meta { margin-top: 4px; font: 9.5px/1.5 'JetBrains Mono', monospace; color: rgba(255,255,255,0.28); }
|
|
360
|
+
.cp-worker-end .worker-end-note { margin-top: 4px; font: 10.5px/1.5 Inter, sans-serif; color: var(--t-muted); }
|
|
361
|
+
.cp-worker-end .post-plan-actions { margin-top: 8px; }
|
|
362
|
+
.cp-worker-end .post-plan-action:disabled { opacity: 0.45; cursor: default; }
|
|
363
|
+
.cp-worker-end .post-plan-action:focus-visible { outline: 1px solid rgba(255,255,255,0.45); outline-offset: 2px; }
|
|
349
364
|
`;
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun Neural Interface — Code editor: AI autocomplete, the decisions
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// What AI autocomplete decides without a page: whether a request may be made at
|
|
6
|
+
// all, which text around the caret is sent, which files and texts are never
|
|
7
|
+
// sent, what is left of a suggestion while the user types it, and what the
|
|
8
|
+
// status-bar chip says. editor-ai.js acts on these; the server's
|
|
9
|
+
// lib/editor-completion.js imports the limits and the never-sent rules from
|
|
10
|
+
// here, so the two sides cannot disagree about them. No DOM and no imports:
|
|
11
|
+
// loaded by `node --test` as it is.
|
|
12
|
+
|
|
13
|
+
/** Sizes of what a request carries (docs/design/code-editor/SPEC.md, section 9.3). */
|
|
14
|
+
export const AI_LIMITS = Object.freeze({
|
|
15
|
+
prefixChars: 6000, // before the caret, cut at a line start
|
|
16
|
+
suffixChars: 1500, // after the caret, cut at a line end
|
|
17
|
+
headerLines: 40, // the file's first lines, when the prefix does not start at offset 0
|
|
18
|
+
headerChars: 4000,
|
|
19
|
+
neighborCount: 2, // snippets of other open tabs
|
|
20
|
+
neighborChars: 1200,
|
|
21
|
+
pathChars: 1000,
|
|
22
|
+
completionChars: 1200, // the most a suggestion holds
|
|
23
|
+
cacheSize: 20, // results kept
|
|
24
|
+
cacheBefore: 300, // characters before the caret in a cache key
|
|
25
|
+
cacheAfter: 100, // characters after it
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// The prefix starts on a multiple of this many characters (then at the next line start), so it stays the same
|
|
29
|
+
// text for a while as the caret moves on: a provider that caches the start of a prompt can reuse it.
|
|
30
|
+
const PREFIX_GRID = 1000;
|
|
31
|
+
|
|
32
|
+
/** What may follow the caret on its line for a suggestion to be asked for: nothing, or closing punctuation. */
|
|
33
|
+
const CLOSING_ONLY = /^[\s)\]}'"`;,]*$/;
|
|
34
|
+
|
|
35
|
+
const PRIVATE_KEY = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/;
|
|
36
|
+
|
|
37
|
+
// ── Never sent ──
|
|
38
|
+
|
|
39
|
+
/** The last part of a path, whichever slash it uses. */
|
|
40
|
+
export function fileNameOf(path) {
|
|
41
|
+
const text = String(path ?? '');
|
|
42
|
+
return text.slice(Math.max(text.lastIndexOf('/'), text.lastIndexOf('\\')) + 1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function globToRegExp(glob) {
|
|
46
|
+
const body = String(glob).replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.');
|
|
47
|
+
return new RegExp(`^${body}$`, 'i');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whether a file is one AI autocomplete never reads. The globs (`*` any run of characters, `?` one) are matched
|
|
52
|
+
* on the file's name, not on its folder, whatever the case of its letters.
|
|
53
|
+
* @param {string} path a path or a bare file name
|
|
54
|
+
* @param {string[]} globs `ai.exclude`
|
|
55
|
+
*/
|
|
56
|
+
export function matchesExclude(path, globs) {
|
|
57
|
+
const name = fileNameOf(path);
|
|
58
|
+
if (!name || !Array.isArray(globs)) return false;
|
|
59
|
+
return globs.some((glob) => typeof glob === 'string' && glob && globToRegExp(glob).test(name));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Whether any of the texts holds the first line of a private key. */
|
|
63
|
+
export function hasPrivateKey(...texts) {
|
|
64
|
+
return texts.some((text) => typeof text === 'string' && PRIVATE_KEY.test(text));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── When to ask ──
|
|
68
|
+
|
|
69
|
+
/** Whether the rest of the caret's line lets a suggestion in: empty, or only closing punctuation. */
|
|
70
|
+
export function restOfLineAllows(after) {
|
|
71
|
+
return CLOSING_ONLY.test(String(after ?? ''));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Whether a suggestion may be asked for right now.
|
|
76
|
+
* @param {object} s
|
|
77
|
+
* @param {boolean} s.enabled `ai.enabled`
|
|
78
|
+
* @param {boolean} s.capable the server has the `complete` capability
|
|
79
|
+
* @param {boolean} [s.readOnly] the document cannot be edited (a save is under way)
|
|
80
|
+
* @param {boolean} s.focused the document has the keyboard
|
|
81
|
+
* @param {boolean} s.visible the page is visible
|
|
82
|
+
* @param {number} s.carets how many carets or selections there are
|
|
83
|
+
* @param {boolean} s.hasSelection any of them selects text
|
|
84
|
+
* @param {boolean} s.composing an IME composition is under way
|
|
85
|
+
* @param {string} s.lineAfter the text after the caret on its line
|
|
86
|
+
* @param {string} s.path the file's path
|
|
87
|
+
* @param {string[]} s.exclude `ai.exclude`
|
|
88
|
+
* @returns {{ ok: true } | { ok: false, reason: 'off' | 'read-only' | 'unfocused' | 'hidden' | 'multiple' | 'selection'
|
|
89
|
+
* | 'composing' | 'excluded' | 'mid-line' }} An open completion list is no reason to wait: the suggestion is
|
|
90
|
+
* asked for, and kept hidden until the list closes.
|
|
91
|
+
*/
|
|
92
|
+
export function shouldRequest(s = {}) {
|
|
93
|
+
const no = (reason) => ({ ok: false, reason });
|
|
94
|
+
if (!s.enabled || !s.capable) return no('off');
|
|
95
|
+
if (!s.path || matchesExclude(s.path, s.exclude)) return no('excluded');
|
|
96
|
+
if (s.readOnly) return no('read-only');
|
|
97
|
+
if (!s.focused) return no('unfocused');
|
|
98
|
+
if (!s.visible) return no('hidden');
|
|
99
|
+
if (s.carets !== 1) return no('multiple');
|
|
100
|
+
if (s.hasSelection) return no('selection');
|
|
101
|
+
if (s.composing) return no('composing');
|
|
102
|
+
if (!restOfLineAllows(s.lineAfter)) return no('mid-line');
|
|
103
|
+
return { ok: true };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Whether suggestions are expected for a document: the feature is on and the server has it, the file is not one
|
|
108
|
+
* that is never read, and there is a model to ask. While they are, the completion list leaves the words of the
|
|
109
|
+
* document out unless it is asked for them (Ctrl-Space): a list that opened for every word would cover the
|
|
110
|
+
* suggestion each time.
|
|
111
|
+
* @param {{ capable: boolean, enabled: boolean, excluded?: boolean, problem?: { kind: string } | null }} s
|
|
112
|
+
*/
|
|
113
|
+
export function suggestionsExpected({ capable, enabled, excluded = false, problem = null } = {}) {
|
|
114
|
+
if (!capable || !enabled || excluded) return false;
|
|
115
|
+
return problem?.kind !== 'no-model' && problem?.kind !== 'no-endpoint';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── What is sent ──
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The text around the caret that a request carries.
|
|
122
|
+
* @param {object} o
|
|
123
|
+
* @param {(from: number, to: number) => string} o.slice reads part of the document (line breaks as "\n")
|
|
124
|
+
* @param {number} o.length the document's length
|
|
125
|
+
* @param {number} o.pos the caret
|
|
126
|
+
* @returns {{ prefix: string, suffix: string, header: string, prefixStart: number, suffixEnd: number }}
|
|
127
|
+
* `prefix`: up to 6,000 characters before the caret, from a line start; `suffix`: up to 1,500 after it, to a
|
|
128
|
+
* line end; `header`: the file's first 40 lines when the prefix does not reach the start of the file, else ''.
|
|
129
|
+
*/
|
|
130
|
+
export function completionWindow({ slice, length, pos }) {
|
|
131
|
+
const at = Math.max(0, Math.min(length, pos));
|
|
132
|
+
let prefixStart = Math.ceil(Math.max(0, at - AI_LIMITS.prefixChars) / PREFIX_GRID) * PREFIX_GRID;
|
|
133
|
+
let prefix = slice(prefixStart, at);
|
|
134
|
+
if (prefixStart > 0) {
|
|
135
|
+
const lineStart = prefix.indexOf('\n');
|
|
136
|
+
if (lineStart >= 0) { prefixStart += lineStart + 1; prefix = prefix.slice(lineStart + 1); }
|
|
137
|
+
}
|
|
138
|
+
let suffixEnd = Math.min(length, at + AI_LIMITS.suffixChars);
|
|
139
|
+
let suffix = slice(at, suffixEnd);
|
|
140
|
+
if (suffixEnd < length) {
|
|
141
|
+
const lineEnd = suffix.lastIndexOf('\n');
|
|
142
|
+
if (lineEnd >= 0) { suffixEnd = at + lineEnd; suffix = suffix.slice(0, lineEnd); }
|
|
143
|
+
}
|
|
144
|
+
// The header stops where the prefix begins: no line is sent twice.
|
|
145
|
+
const header = prefixStart > 0 ? clampHeader(slice(0, Math.min(prefixStart, AI_LIMITS.headerChars + 1))) : '';
|
|
146
|
+
return { prefix, suffix, header, prefixStart, suffixEnd };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** At most the last 6,000 characters of a prefix, from a line start. */
|
|
150
|
+
export function clampPrefix(text) {
|
|
151
|
+
const prefix = String(text ?? '');
|
|
152
|
+
if (prefix.length <= AI_LIMITS.prefixChars) return prefix;
|
|
153
|
+
const cut = prefix.slice(prefix.length - AI_LIMITS.prefixChars);
|
|
154
|
+
const lineStart = cut.indexOf('\n');
|
|
155
|
+
return lineStart >= 0 ? cut.slice(lineStart + 1) : cut;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** At most the first 1,500 characters of a suffix, to a line end. */
|
|
159
|
+
export function clampSuffix(text) {
|
|
160
|
+
const suffix = String(text ?? '');
|
|
161
|
+
if (suffix.length <= AI_LIMITS.suffixChars) return suffix;
|
|
162
|
+
const cut = suffix.slice(0, AI_LIMITS.suffixChars);
|
|
163
|
+
const lineEnd = cut.lastIndexOf('\n');
|
|
164
|
+
return lineEnd >= 0 ? cut.slice(0, lineEnd) : cut;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** At most the first 40 lines and 4,000 characters of a header, whole lines only. */
|
|
168
|
+
export function clampHeader(text) {
|
|
169
|
+
let header = String(text ?? '').split('\n').slice(0, AI_LIMITS.headerLines).join('\n');
|
|
170
|
+
if (header.length > AI_LIMITS.headerChars) {
|
|
171
|
+
const cut = header.slice(0, AI_LIMITS.headerChars);
|
|
172
|
+
const lineEnd = cut.lastIndexOf('\n');
|
|
173
|
+
header = lineEnd >= 0 ? cut.slice(0, lineEnd) : cut;
|
|
174
|
+
}
|
|
175
|
+
return header.replace(/\n+$/, '');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The body of `POST /api/editor/complete`.
|
|
180
|
+
* @param {object} o
|
|
181
|
+
* @param {string} o.path
|
|
182
|
+
* @param {string} [o.language] the language's name as the status bar shows it
|
|
183
|
+
* @param {string} [o.indentUnit] one level of indentation (spaces, or a tab)
|
|
184
|
+
* @param {{ prefix: string, suffix: string, header: string }} o.window from completionWindow()
|
|
185
|
+
* @param {{ path: string, text: string }[]} [o.neighbors]
|
|
186
|
+
* @param {'auto' | 'manual'} [o.trigger]
|
|
187
|
+
*/
|
|
188
|
+
export function requestBody({ path, language = '', indentUnit = ' ', window, neighbors = [], trigger = 'auto' }) {
|
|
189
|
+
const body = { path, language, indentUnit, prefix: window.prefix, suffix: window.suffix, trigger: trigger === 'manual' ? 'manual' : 'auto' };
|
|
190
|
+
if (window.header) body.header = window.header;
|
|
191
|
+
if (neighbors.length) body.neighbors = neighbors.slice(0, AI_LIMITS.neighborCount).map((n) => ({ path: n.path, text: String(n.text).slice(0, AI_LIMITS.neighborChars) }));
|
|
192
|
+
return body;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The part of another open document that is sent along: the 1,200 characters around its caret (the whole text
|
|
197
|
+
* when it is shorter), whole lines where the window allows.
|
|
198
|
+
* @param {{ slice: (from: number, to: number) => string, length: number, head?: number }} doc
|
|
199
|
+
*/
|
|
200
|
+
export function neighborSnippet({ slice, length, head = 0 }) {
|
|
201
|
+
const size = AI_LIMITS.neighborChars;
|
|
202
|
+
if (length <= size) return slice(0, length);
|
|
203
|
+
const from = Math.max(0, Math.min(length - size, Math.floor(Number(head) || 0) - size / 2));
|
|
204
|
+
let text = slice(from, from + size);
|
|
205
|
+
if (from > 0) {
|
|
206
|
+
const lineStart = text.indexOf('\n');
|
|
207
|
+
if (lineStart >= 0 && lineStart < text.length - 1) text = text.slice(lineStart + 1);
|
|
208
|
+
}
|
|
209
|
+
if (from + size < length) {
|
|
210
|
+
const lineEnd = text.lastIndexOf('\n');
|
|
211
|
+
if (lineEnd > 0) text = text.slice(0, lineEnd);
|
|
212
|
+
}
|
|
213
|
+
return text;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Snippets of other open tabs for a request: up to two, of the document's own language, the tabs used last
|
|
218
|
+
* first. Never a file on the excluded list, never a text that holds a private key, never without a language
|
|
219
|
+
* (plain text is not code to learn from).
|
|
220
|
+
* @param {object} o
|
|
221
|
+
* @param {{ path: string, language: string }} o.current the document the request is for
|
|
222
|
+
* @param {{ path: string, language: string, length: number, head?: number, slice: (from: number, to: number) => string }[]} o.tabs
|
|
223
|
+
* the other open documents, the most recently used first
|
|
224
|
+
* @param {string[]} o.exclude `ai.exclude`
|
|
225
|
+
* @returns {{ path: string, text: string }[]}
|
|
226
|
+
*/
|
|
227
|
+
export function pickNeighbors({ current, tabs, exclude } = {}) {
|
|
228
|
+
const out = [];
|
|
229
|
+
if (!current?.language) return out;
|
|
230
|
+
for (const tab of tabs || []) {
|
|
231
|
+
if (out.length >= AI_LIMITS.neighborCount) break;
|
|
232
|
+
if (!tab || !tab.path || tab.path === current.path || tab.language !== current.language) continue;
|
|
233
|
+
if (matchesExclude(tab.path, exclude) || out.some((taken) => taken.path === tab.path)) continue;
|
|
234
|
+
const text = neighborSnippet(tab);
|
|
235
|
+
if (!text.trim() || hasPrivateKey(text)) continue;
|
|
236
|
+
out.push({ path: tab.path, text });
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── Results kept ──
|
|
242
|
+
|
|
243
|
+
function fnv1a(text) {
|
|
244
|
+
let hash = 0x811c9dc5;
|
|
245
|
+
for (let i = 0; i < text.length; i++) {
|
|
246
|
+
hash ^= text.charCodeAt(i);
|
|
247
|
+
hash = Math.imul(hash, 0x01000193);
|
|
248
|
+
}
|
|
249
|
+
return (hash >>> 0).toString(16).padStart(8, '0');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** The key a result is kept under: the last 300 characters before the caret, the first 100 after it, and the path. */
|
|
253
|
+
export function cacheKey({ path, prefix, suffix }) {
|
|
254
|
+
const before = String(prefix ?? '').slice(-AI_LIMITS.cacheBefore);
|
|
255
|
+
const after = String(suffix ?? '').slice(0, AI_LIMITS.cacheAfter);
|
|
256
|
+
return `${fnv1a(`${before}\u0000${after}`)}${fnv1a(`${after}\u0000${before}\u0000${path ?? ''}`)}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** The last results, the most recently used last. */
|
|
260
|
+
export function createSuggestionCache(max = AI_LIMITS.cacheSize) {
|
|
261
|
+
const entries = new Map();
|
|
262
|
+
return {
|
|
263
|
+
get(key) {
|
|
264
|
+
if (!entries.has(key)) return undefined;
|
|
265
|
+
const value = entries.get(key);
|
|
266
|
+
entries.delete(key);
|
|
267
|
+
entries.set(key, value);
|
|
268
|
+
return value;
|
|
269
|
+
},
|
|
270
|
+
set(key, value) {
|
|
271
|
+
entries.delete(key);
|
|
272
|
+
entries.set(key, value);
|
|
273
|
+
while (entries.size > max) entries.delete(entries.keys().next().value);
|
|
274
|
+
},
|
|
275
|
+
delete(key) { return entries.delete(key); },
|
|
276
|
+
clear() { entries.clear(); },
|
|
277
|
+
get size() { return entries.size; },
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── A suggestion while the user types ──
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* What is left of a suggestion after a change to the document.
|
|
285
|
+
* @param {{ from: number, text: string } | null} suggestion what is shown: `text` would be inserted at `from`
|
|
286
|
+
* @param {{ from: number, to: number, insert: string, head: number }} change one change: `insert` replaced
|
|
287
|
+
* `from`..`to`, and the caret is at `head` afterwards
|
|
288
|
+
* @returns {{ typed: boolean, suggestion: { from: number, text: string } | null }} `typed`: the user typed what
|
|
289
|
+
* the suggestion begins with (no new request is needed while a remainder is left); `suggestion`: the remainder
|
|
290
|
+
*/
|
|
291
|
+
export function typeThrough(suggestion, change) {
|
|
292
|
+
const miss = { typed: false, suggestion: null };
|
|
293
|
+
if (!suggestion || !change || typeof change.insert !== 'string' || !change.insert) return miss;
|
|
294
|
+
if (change.from !== change.to || change.from !== suggestion.from) return miss;
|
|
295
|
+
if (change.head !== change.from + change.insert.length) return miss;
|
|
296
|
+
if (!suggestion.text.startsWith(change.insert)) return miss;
|
|
297
|
+
const rest = suggestion.text.slice(change.insert.length);
|
|
298
|
+
return { typed: true, suggestion: rest ? { from: change.head, text: rest } : null };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const WORD = /^(\s*)(?:[\p{L}\p{N}_$]+|[^\s\p{L}\p{N}_$]+)/u;
|
|
302
|
+
|
|
303
|
+
/** The part of a suggestion "accept one word" takes: the white space in front of the next word, and that word (or run of punctuation). */
|
|
304
|
+
export function nextWord(text) {
|
|
305
|
+
const value = String(text ?? '');
|
|
306
|
+
const match = WORD.exec(value);
|
|
307
|
+
return match ? match[0] : value;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── The chip ──
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* What the status-bar chip shows.
|
|
314
|
+
* @param {object} s
|
|
315
|
+
* @param {boolean} s.capable the server has the `complete` capability (without it there is no chip)
|
|
316
|
+
* @param {boolean} s.enabled `ai.enabled`
|
|
317
|
+
* @param {boolean} [s.excluded] the open file is one that is never read
|
|
318
|
+
* @param {boolean} [s.busy] a request is under way
|
|
319
|
+
* @param {{ kind: string } | null} [s.problem] the last request's failure, until one succeeds
|
|
320
|
+
* @returns {'hidden' | 'off' | 'ready' | 'thinking' | 'problem'}
|
|
321
|
+
*/
|
|
322
|
+
export function chipState({ capable, enabled, excluded = false, busy = false, problem = null } = {}) {
|
|
323
|
+
if (!capable) return 'hidden';
|
|
324
|
+
if (!enabled || excluded) return 'off';
|
|
325
|
+
if (busy) return 'thinking';
|
|
326
|
+
if (problem) return 'problem';
|
|
327
|
+
return 'ready';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* What a failed request means.
|
|
332
|
+
* @param {{ status?: number, body?: object | null, error?: string | null }} failure what the server answered, or
|
|
333
|
+
* why there was no answer
|
|
334
|
+
* @returns {{ kind: 'disabled' | 'excluded' | 'rate' | 'engine' | 'no-model' | 'no-endpoint' | 'timeout' | 'network' | 'server',
|
|
335
|
+
* retryAfterMs: number, message: string }} `disabled` and `excluded` are not problems: the feature is off, or
|
|
336
|
+
* off for this file. `message` is the engine's own words when it gave any.
|
|
337
|
+
*/
|
|
338
|
+
export function describeFailure({ status = 0, body = null, error = null } = {}) {
|
|
339
|
+
const message = typeof body?.message === 'string' ? body.message.replace(/\s+/g, ' ').trim().slice(0, 300) : '';
|
|
340
|
+
const out = (kind, retryAfterMs = 0) => ({ kind, retryAfterMs, message });
|
|
341
|
+
if (status === 409 && body?.code === 'disabled') return out('disabled');
|
|
342
|
+
if (status === 403 && body?.code === 'excluded') return out('excluded');
|
|
343
|
+
if (status === 429) {
|
|
344
|
+
const wait = Number(body?.retryAfterMs);
|
|
345
|
+
return out('rate', Number.isFinite(wait) && wait > 0 ? Math.min(wait, 120_000) : 5000);
|
|
346
|
+
}
|
|
347
|
+
if (status === 502 && body?.code === 'engine') {
|
|
348
|
+
if (body.reason === 'no-model') return out('no-model');
|
|
349
|
+
if (body.reason === 'no-endpoint') return out('no-endpoint');
|
|
350
|
+
if (body.reason === 'timeout') return out('timeout');
|
|
351
|
+
return out('engine');
|
|
352
|
+
}
|
|
353
|
+
if (!status) return { kind: 'network', retryAfterMs: 0, message: String(error ?? '').slice(0, 300) };
|
|
354
|
+
return out('server');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** The i18n key of a failure's sentence (parameters: `message`, `seconds`). */
|
|
358
|
+
export function failureKey(failure) {
|
|
359
|
+
switch (failure?.kind) {
|
|
360
|
+
case 'rate': return 'editor.ai.problem.rate';
|
|
361
|
+
case 'no-model': return 'editor.ai.problem.noModel';
|
|
362
|
+
case 'no-endpoint': return 'editor.ai.problem.noEndpoint';
|
|
363
|
+
case 'timeout': return 'editor.ai.problem.timeout';
|
|
364
|
+
case 'network': return 'editor.ai.problem.network';
|
|
365
|
+
case 'engine': return failure.message ? 'editor.ai.problem.engine' : 'editor.ai.problem.engineUnknown';
|
|
366
|
+
default: return 'editor.ai.problem.server';
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* The popover's line about today's use, from the answer of `GET /api/editor/complete/usage`.
|
|
372
|
+
* "No requests today" is said only for an answer that says so: no answer, or one that is not one, is a read
|
|
373
|
+
* that failed.
|
|
374
|
+
* @param {{ ok?: boolean, today?: { requests: number, tokensIn: number, tokensOut: number } } | null} answer
|
|
375
|
+
* @returns {{ key: string, params: object }}
|
|
376
|
+
*/
|
|
377
|
+
export function usageLine(answer) {
|
|
378
|
+
const day = answer && answer.ok === true && answer.today && typeof answer.today === 'object' ? answer.today : null;
|
|
379
|
+
if (!day) return { key: 'editor.ai.todayFailed', params: {} };
|
|
380
|
+
const requests = Math.round(Number(day.requests) || 0);
|
|
381
|
+
if (requests <= 0) return { key: 'editor.ai.todayNone', params: {} };
|
|
382
|
+
return {
|
|
383
|
+
key: requests === 1 ? 'editor.ai.today.one' : 'editor.ai.today.other',
|
|
384
|
+
params: { count: formatCount(requests), input: formatCount(day.tokensIn), output: formatCount(day.tokensOut) },
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Whether the popover's read of the models failed. They are asked for with the OpenCode engine only; an answer
|
|
390
|
+
* that says OpenCode is not running is an answer (it has a sentence of its own).
|
|
391
|
+
* @param {{ engine: string, models: object | null }} s `models`: the answer of `GET /api/editor/complete/models`
|
|
392
|
+
*/
|
|
393
|
+
export function modelsReadFailed({ engine, models } = {}) {
|
|
394
|
+
return engine !== 'endpoint' && !(models && models.ok === true);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** A count the way the popover writes it: 950, 1.2k, 3.4M. */
|
|
398
|
+
export function formatCount(value) {
|
|
399
|
+
const n = Math.max(0, Math.round(Number(value) || 0));
|
|
400
|
+
if (n < 1000) return String(n);
|
|
401
|
+
if (n < 1_000_000) return `${(n / 1000).toFixed(n < 10_000 ? 1 : 0).replace(/\.0$/, '')}k`;
|
|
402
|
+
return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, '')}M`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** A duration the way a model's test result is written: 640 ms, 1.2 s. */
|
|
406
|
+
export function formatMs(value) {
|
|
407
|
+
const ms = Math.max(0, Math.round(Number(value) || 0));
|
|
408
|
+
return ms < 1000 ? `${ms} ms` : `${(ms / 1000).toFixed(1)} s`;
|
|
409
|
+
}
|