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
|
@@ -15,6 +15,7 @@ import { createClaudeBrain } from './assistant-brains/claude.js';
|
|
|
15
15
|
import { createCodexBrain } from './assistant-brains/codex.js';
|
|
16
16
|
import { createOpenCodeBrain } from './assistant-brains/opencode.js';
|
|
17
17
|
import { buildAssistantPersona, formatMailbox, reconcileRouteItems } from './assistant-persona.js';
|
|
18
|
+
import { normalizeTestDensity, testDensityStamp } from './assistant-test-density.js';
|
|
18
19
|
import { codexUsageCostUsd } from './assistant-budget.js';
|
|
19
20
|
import { CLAUDE_ESTIMATE_SOURCE, createClaudeMeter, createCodexMeter, createOpenCodeMeter, totalTokens, withTotal, zeroTokens } from './assistant-usage.js';
|
|
20
21
|
import { claudeTokensCostUsd } from './assistant-pricing.js';
|
|
@@ -192,6 +193,11 @@ export function createAssistantRuntime({
|
|
|
192
193
|
// Clarification (assistant-clarify.js): human prompts, the brain's own
|
|
193
194
|
// question cards and the clarify cards' answers reach it from here.
|
|
194
195
|
clarifier = null,
|
|
196
|
+
// The user-context engine (assistant-user-context.js): what the Assistant knows about the
|
|
197
|
+
// user, nudges from recent conversations, the learning loop and the per-turn trace.
|
|
198
|
+
userContext = null,
|
|
199
|
+
// lib/greeting/ engine (or null): the note about the greeting a conversation opened with, and when the person was last here.
|
|
200
|
+
greeting = null,
|
|
195
201
|
// Remote sessions (lib/remote-policy.js): the level registry shared with the
|
|
196
202
|
// dispatcher and the WhatsApp bridge, and the registered project paths a
|
|
197
203
|
// remote session's workers must run in.
|
|
@@ -340,6 +346,10 @@ export function createAssistantRuntime({
|
|
|
340
346
|
// Closing (endSessionWork): the close under way (a promise), and the generation it started.
|
|
341
347
|
gen: 0, work: new Map(), workActive: null, workTimer: null, workPumpQueued: false, routeGens: new Map(),
|
|
342
348
|
closing: null, closedGen: 0,
|
|
349
|
+
// User context (assistant-user-context.js): what the live brain was built for (a layer
|
|
350
|
+
// switched since restarts an idle brain), the human turns sent and not ended yet (oldest
|
|
351
|
+
// first), and the last reply (context for what the next message teaches).
|
|
352
|
+
brainUserContext: null, ucTurns: [], lastReply: '',
|
|
343
353
|
};
|
|
344
354
|
}
|
|
345
355
|
// ── remote sessions (lib/remote-policy.js) ─────────────────────────────────
|
|
@@ -727,6 +737,12 @@ export function createAssistantRuntime({
|
|
|
727
737
|
*/
|
|
728
738
|
function brainNeedsRestart(live) {
|
|
729
739
|
if (!live?.brain) return false;
|
|
740
|
+
// Its persona was built with (or without) a user-context layer that was switched since.
|
|
741
|
+
if (userContext && live.brainUserContext !== null) {
|
|
742
|
+
let key = null;
|
|
743
|
+
try { key = userContext.brainKey(live.record); } catch { key = null; }
|
|
744
|
+
if (key !== null && key !== live.brainUserContext) return true;
|
|
745
|
+
}
|
|
730
746
|
const policy = remotePolicyFor(live);
|
|
731
747
|
// A Codex / OpenCode brain built before the session became remote is not read-only yet.
|
|
732
748
|
if (live.brainProvider !== 'claude-code') return !!policy && live.brainRemoteReadOnly !== true;
|
|
@@ -747,6 +763,8 @@ export function createAssistantRuntime({
|
|
|
747
763
|
const effectiveMode = policy ? clampRouteMode(policy, mode || defaultRouteMode(), { now: now() }) : (mode || defaultRouteMode());
|
|
748
764
|
return { mode, effectiveMode, defaultMode: defaultRouteMode(), askBelow };
|
|
749
765
|
}
|
|
766
|
+
/** The session's Test density: its own value, else the default level (assistant-test-density.js). */
|
|
767
|
+
function testDensityOf(record) { return normalizeTestDensity(record?.testDensity); }
|
|
750
768
|
function computerSupported() { try { return !!desktop?.isSupported?.(); } catch { return false; } }
|
|
751
769
|
/**
|
|
752
770
|
* Effective computer toggle: explicit per-session value, else the service default (ON after setup).
|
|
@@ -825,6 +843,8 @@ export function createAssistantRuntime({
|
|
|
825
843
|
function publicRecord(record, { withLive = true, live = null, persisted = false } = {}) {
|
|
826
844
|
const out = { ...record };
|
|
827
845
|
if (!persisted) delete out.usageState;
|
|
846
|
+
// The user-context state (frozen digest, delivery ledgers) is the server's own.
|
|
847
|
+
if (!persisted) delete out.userContext;
|
|
828
848
|
if (withLive && live) {
|
|
829
849
|
out.running = live.running;
|
|
830
850
|
out.attached = live.sockets.size;
|
|
@@ -840,7 +860,10 @@ export function createAssistantRuntime({
|
|
|
840
860
|
// A remote session's computer use is not a per-conversation choice: no explicit value, and why it is what it is.
|
|
841
861
|
out.computerUseExplicit = policy ? null : (record.computerUse === true || record.computerUse === false ? record.computerUse : null);
|
|
842
862
|
out.computerRemote = computer ? { state: computer.state, reason: computer.reason, level: computer.level, approved: live.computerTurn?.approved === true } : null;
|
|
843
|
-
out.
|
|
863
|
+
out.testDensity = testDensityOf(record);
|
|
864
|
+
out.testDensityExplicit = normalizeTestDensity(record.testDensity, null);
|
|
865
|
+
// `rename`: this server keeps a rename on the record (userRenamed), so a panel writes it here.
|
|
866
|
+
out.features = { routing: !!router, computer: computerSupported(), testDensity: true, rename: true };
|
|
844
867
|
out.budget = sessionMoney(record.id);
|
|
845
868
|
out.remoteLevel = policy ? effectiveLevel(policy, { untrusted: live.turnUntrusted === true, now: now() }) : null;
|
|
846
869
|
}
|
|
@@ -1187,6 +1210,8 @@ export function createAssistantRuntime({
|
|
|
1187
1210
|
// The Claude CLI's own cap stopped the brain: its process cannot run another turn.
|
|
1188
1211
|
if (event.subtype === 'error_max_budget_usd' && live.brainProvider === 'claude-code') live.brainCapHit = true;
|
|
1189
1212
|
if (live.planTurn) live.planText = planTurnText(event);
|
|
1213
|
+
// The reply the user's next message answers (context for the learning loop; a subagent's result is not it).
|
|
1214
|
+
if (typeof event.result === 'string' && event.result.trim() && !event.parent_tool_use_id) live.lastReply = clip(event.result, 600);
|
|
1190
1215
|
touch(live);
|
|
1191
1216
|
}
|
|
1192
1217
|
if (TRANSCRIPT_EVENT_TYPES.has(event.type) && !(event.type === 'system' && event.subtype !== 'init')) appendTranscript(id, { at: iso(now()), packet });
|
|
@@ -1292,8 +1317,32 @@ export function createAssistantRuntime({
|
|
|
1292
1317
|
}
|
|
1293
1318
|
}
|
|
1294
1319
|
sendToSockets(live, packet);
|
|
1320
|
+
// After the packet: the turn's trace moves under the reply the panel has just closed.
|
|
1321
|
+
if ((packet.type === 'done' || packet.type === 'aborted' || packet.type === 'error') && !live.running) finishUserContextTurn(live, packet.type);
|
|
1295
1322
|
if (plan) offerPlanCard(live, plan, planGen);
|
|
1296
1323
|
}
|
|
1324
|
+
/** The oldest human turn still open ended: its trace moves under the reply and learning starts, off the reply path. */
|
|
1325
|
+
function finishUserContextTurn(live, outcome) {
|
|
1326
|
+
const handle = live.ucTurns.shift();
|
|
1327
|
+
if (!handle || !userContext) return;
|
|
1328
|
+
try { userContext.endTurn(live.record, handle, { outcome }); } catch (error) { log('assistant:user-context', error?.message || String(error)); }
|
|
1329
|
+
}
|
|
1330
|
+
/** One turn's user-context trace: to the panel and into the journal, a full snapshot each time (the panel upserts by traceId). */
|
|
1331
|
+
function emitAutomation(live, event) {
|
|
1332
|
+
if (!event || sessions.get(live.record.id) !== live) return;
|
|
1333
|
+
const packet = { type: 'event', event };
|
|
1334
|
+
sendToSockets(live, packet);
|
|
1335
|
+
appendTranscript(live.record.id, { at: iso(now()), packet });
|
|
1336
|
+
}
|
|
1337
|
+
/** The cards of a session left unanswered right now: the runtime's plan card, route cards, clarify cards. */
|
|
1338
|
+
function openCards(live) {
|
|
1339
|
+
const out = [];
|
|
1340
|
+
const title = (card) => clip(card?.request?.title || card?.request?.summary || card?.request?.route?.summary || card?.request?.questions?.[0]?.question || '', 80);
|
|
1341
|
+
if (live.planCard?.requestId) out.push({ id: String(live.planCard.requestId), kind: 'plan', title: clip(String(live.planCard.plan || '').split('\n')[0], 80) });
|
|
1342
|
+
try { for (const card of router?.pendingCards?.(live.record.id) || []) if (card?.request_id) out.push({ id: String(card.request_id), kind: 'route', title: title(card) }); } catch {}
|
|
1343
|
+
try { for (const card of clarifier?.pendingCards?.(live.record.id) || []) if (card?.request_id) out.push({ id: String(card.request_id), kind: 'question', title: title(card) }); } catch {}
|
|
1344
|
+
return out;
|
|
1345
|
+
}
|
|
1297
1346
|
/**
|
|
1298
1347
|
* Codex and OpenCode end a plan turn with the plan instead of asking for
|
|
1299
1348
|
* approval the way Claude's ExitPlanMode does: the runtime asks, with the same
|
|
@@ -1703,6 +1752,8 @@ export function createAssistantRuntime({
|
|
|
1703
1752
|
try { if (catalogValue?.models) sheet = selectSheetModels(catalogValue, { brain, preferences: routingCfg.preferences || {}, max: 8 }); } catch {}
|
|
1704
1753
|
let setupState = null;
|
|
1705
1754
|
try { setupState = desktop?.setupState?.() || null; } catch {}
|
|
1755
|
+
// This prompt states the level: a "[SynaBun Tests]" line follows only a later change.
|
|
1756
|
+
live.testDensityStamp = testDensityStamp(testDensityOf(live.record));
|
|
1706
1757
|
return buildAssistantPersona({
|
|
1707
1758
|
assistantSessionId: live.record.id,
|
|
1708
1759
|
brain,
|
|
@@ -1721,6 +1772,7 @@ export function createAssistantRuntime({
|
|
|
1721
1772
|
// A remote session: what remoteComputerUse says for it (a turn with untrusted content is asked either way).
|
|
1722
1773
|
computer: { available: computerSupported(), enabled: computerEffective(live.record), setupState, remote: computerDecision(live, { untrusted: false }) },
|
|
1723
1774
|
taskClasses: TASK_CLASS_META,
|
|
1775
|
+
testDensity: testDensityOf(live.record),
|
|
1724
1776
|
clarify: !!clarifier,
|
|
1725
1777
|
channel: live.record.channel || live.record.remote?.channel || null,
|
|
1726
1778
|
});
|
|
@@ -1742,9 +1794,18 @@ export function createAssistantRuntime({
|
|
|
1742
1794
|
if (remote) live.record.brain = clampedBrain(live, live.record.brain);
|
|
1743
1795
|
const brain = live.record.brain;
|
|
1744
1796
|
const sink = { send: (packet) => onBrainPacket(live, packet) };
|
|
1745
|
-
const
|
|
1797
|
+
const basePersona = await personaFor(live);
|
|
1746
1798
|
if (stale()) return null;
|
|
1747
1799
|
if (live.brain) return live.brain;
|
|
1800
|
+
// What SynaBun has learned about the user rides after the persona, in the same system-level
|
|
1801
|
+
// slot (Claude systemPromptAppend, Codex developerInstructions, OpenCode agent prompt), frozen
|
|
1802
|
+
// per provider conversation so the prompt cache holds.
|
|
1803
|
+
let userSuffix = { text: '', key: null, nudges: false };
|
|
1804
|
+
if (userContext) {
|
|
1805
|
+
try { userSuffix = userContext.personaSuffix(live.record) || userSuffix; }
|
|
1806
|
+
catch (error) { log('assistant:user-context', error?.message || String(error)); }
|
|
1807
|
+
}
|
|
1808
|
+
const persona = userSuffix.text ? `${basePersona}\n\n${userSuffix.text}` : basePersona;
|
|
1748
1809
|
live.personaText = persona;
|
|
1749
1810
|
// Computer use: one unforgeable grant per brain (the MCP `computer` group is
|
|
1750
1811
|
// advertised only to callers presenting it). Revoked when the brain goes.
|
|
@@ -1790,6 +1851,8 @@ export function createAssistantRuntime({
|
|
|
1790
1851
|
// A Codex / OpenCode brain of a remote session is read-only for good: the
|
|
1791
1852
|
// read-only sandbox (Codex), the plan agent (OpenCode), no auto-accept.
|
|
1792
1853
|
remoteReadOnly: !!remote && remoteBrainLimited(brain.provider),
|
|
1854
|
+
// The engine keeps this conversation's thread record: pre-compact.mjs asks for no conversation memory.
|
|
1855
|
+
userContextThreads: brain.provider === 'claude-code' && userSuffix.nudges === true,
|
|
1793
1856
|
};
|
|
1794
1857
|
const gate = ensureGate(live);
|
|
1795
1858
|
// A remote session's hook / plugin refuses a call it could not check (fail closed).
|
|
@@ -1805,11 +1868,14 @@ export function createAssistantRuntime({
|
|
|
1805
1868
|
const memoryHooks = brain.provider === 'claude-code' && memory?.claudeHooks ? memory.claudeHooks({
|
|
1806
1869
|
session: live.record.id, project: live.record.project || 'global',
|
|
1807
1870
|
getGeneration: () => live.record.memoryGeneration || 1,
|
|
1808
|
-
bumpGeneration: () => { live.record.memoryGeneration = (live.record.memoryGeneration || 1) + 1; touch(live); },
|
|
1871
|
+
bumpGeneration: () => { live.record.memoryGeneration = (live.record.memoryGeneration || 1) + 1; try { userContext?.onCompaction(live.record); } catch {} touch(live); },
|
|
1809
1872
|
markRemembered: () => { live.rememberedThisTurn = true; live.obligations.clear(); },
|
|
1810
1873
|
hasPendingObligations: () => live.obligations.size > 0 && !live.rememberedThisTurn,
|
|
1811
1874
|
onRecall: (recalled) => { if (recalled?.results?.length || recalled?.alreadyPresent) sendToSockets(live, { type: 'event', event: { type: 'synabun.memories', memories: recalled.results || [], alreadyPresent: !!recalled.alreadyPresent } }); },
|
|
1812
1875
|
describeTurn: (text) => describeTurn(live, text),
|
|
1876
|
+
// User context: its categories stay out of Related Memories, and its per-turn block follows them.
|
|
1877
|
+
divert: () => userContext?.divertCategories() || null,
|
|
1878
|
+
turnContext: ({ recalled, meta }) => (userContext && meta?.uc ? userContext.turnBlock(live.record, meta.uc, recalled).text : ''),
|
|
1813
1879
|
}) : null;
|
|
1814
1880
|
// Claude: memory, the route gate, the browser policy (every session: plan mode and subagents too), a remote session's hook.
|
|
1815
1881
|
const claude = brain.provider === 'claude-code';
|
|
@@ -1821,6 +1887,7 @@ export function createAssistantRuntime({
|
|
|
1821
1887
|
live.brainSkipAllowed = remote ? deps.remoteLevel === 'autonomous' : null;
|
|
1822
1888
|
live.brainComputerMode = computerMode;
|
|
1823
1889
|
live.brainRemoteReadOnly = deps.remoteReadOnly === true;
|
|
1890
|
+
live.brainUserContext = userSuffix.key;
|
|
1824
1891
|
live.record.status = 'idle';
|
|
1825
1892
|
touch(live);
|
|
1826
1893
|
try { await created.start?.(); } catch (error) {
|
|
@@ -1858,6 +1925,9 @@ export function createAssistantRuntime({
|
|
|
1858
1925
|
live.running = false;
|
|
1859
1926
|
closePlanCard(live, 'brain_disposed');
|
|
1860
1927
|
live.planTurn = false;
|
|
1928
|
+
live.brainUserContext = null;
|
|
1929
|
+
// A human turn the brain took with it never ends on its own: its trace and its learning close here.
|
|
1930
|
+
for (const handle of live.ucTurns.splice(0)) { try { userContext?.endTurn(live.record, handle, { outcome: 'aborted' }); } catch {} }
|
|
1861
1931
|
// A remote session's computer use ends with its brain: the turn's approval, its open request, the live grant.
|
|
1862
1932
|
endComputerTurn(live, reason === 'closed' ? 'session_closed' : 'brain_disposed');
|
|
1863
1933
|
settleNativeQuestions(live, 'brain_disposed');
|
|
@@ -1990,7 +2060,7 @@ export function createAssistantRuntime({
|
|
|
1990
2060
|
// `offerPlan`: a system turn that revises a plan still ends with a plan card.
|
|
1991
2061
|
// `work`: the scheduler entry this system turn is; `request`: { id, reply } of a
|
|
1992
2062
|
// prompt a socket sent (refuse); `turnGen`: the generation runQuery entered in.
|
|
1993
|
-
async function runTurn(live, { text, images = [], cwd = null, model = null, effort = null, permissionMode = null, planMode = null, system = false, taskId = null, turnModel = null, turnEffort = null, gateOpen = false, gateCarry = false, origin = null, authority = null, untrusted = false, offerPlan = false, work = null, request = null, turnGen = live.gen, computerRoutes = null } = {}) {
|
|
2063
|
+
async function runTurn(live, { text, images = [], cwd = null, model = null, effort = null, permissionMode = null, planMode = null, system = false, taskId = null, turnModel = null, turnEffort = null, gateOpen = false, gateCarry = false, origin = null, authority = null, untrusted = false, offerPlan = false, work = null, request = null, turnGen = live.gen, computerRoutes = null, typed = null, greetingId = null } = {}) {
|
|
1994
2064
|
const prompt = String(text || '').trim();
|
|
1995
2065
|
if (!prompt && !(Array.isArray(images) && images.length)) return refuse(live, 'EMPTY_PROMPT', 'No prompt provided', { packet: { type: 'error', message: 'No prompt provided' }, request });
|
|
1996
2066
|
// The scheduler's turn goes once a Stop, the session closing or a newer
|
|
@@ -2049,6 +2119,8 @@ export function createAssistantRuntime({
|
|
|
2049
2119
|
return refuse(live, 'ASSISTANT_BUSY', BUSY_MESSAGE, { packet: { type: 'error', message: BUSY_MESSAGE, code: 'ASSISTANT_BUSY' }, request });
|
|
2050
2120
|
}
|
|
2051
2121
|
let finalText = prompt;
|
|
2122
|
+
// A Codex / OpenCode turn's recall result, for the user-context block below.
|
|
2123
|
+
let turnRecalled = null;
|
|
2052
2124
|
if (!system) {
|
|
2053
2125
|
live.lastPrompt = prompt;
|
|
2054
2126
|
// A prompt from elsewhere (WhatsApp, the API) is news to the panel: it goes live too.
|
|
@@ -2057,7 +2129,8 @@ export function createAssistantRuntime({
|
|
|
2057
2129
|
if (origin && origin !== 'ui') sendToSockets(live, promptPacket);
|
|
2058
2130
|
if (live.brainProvider !== 'claude-code' && memory?.recallForPrompt) {
|
|
2059
2131
|
let recalled = null;
|
|
2060
|
-
try { recalled = await memory.recallForPrompt({ prompt, project: live.record.project, session: live.record.id, generation: live.record.memoryGeneration || 1 }); } catch {}
|
|
2132
|
+
try { recalled = await memory.recallForPrompt({ prompt, project: live.record.project, session: live.record.id, generation: live.record.memoryGeneration || 1, divert: userContext?.divertCategories() || null }); } catch {}
|
|
2133
|
+
turnRecalled = recalled;
|
|
2061
2134
|
// Closed while it recalled: the session that reopened hears nothing of this prompt, and its record keeps what came since.
|
|
2062
2135
|
if (cancelled()) return dropTurn(live, request);
|
|
2063
2136
|
try {
|
|
@@ -2087,6 +2160,16 @@ export function createAssistantRuntime({
|
|
|
2087
2160
|
}
|
|
2088
2161
|
} catch {}
|
|
2089
2162
|
}
|
|
2163
|
+
// Test density changed since the brain last saw it (the same place, after the router's line).
|
|
2164
|
+
// Its prompt states the level it starts with (personaFor), so only a change is a line.
|
|
2165
|
+
try {
|
|
2166
|
+
const tests = testDensityStamp(testDensityOf(live.record));
|
|
2167
|
+
if (live.testDensityStamp === undefined) live.testDensityStamp = tests;
|
|
2168
|
+
else if (live.testDensityStamp !== tests) {
|
|
2169
|
+
finalText = finalText.endsWith(prompt) ? `${finalText.slice(0, finalText.length - prompt.length)}${tests}\n\n${prompt}` : `${tests}\n\n${finalText}`;
|
|
2170
|
+
live.testDensityStamp = tests;
|
|
2171
|
+
}
|
|
2172
|
+
} catch {}
|
|
2090
2173
|
// A new request cycle. Open clarify questions make this message the
|
|
2091
2174
|
// user's reply (assistant-clarify.js); otherwise the last brief retires.
|
|
2092
2175
|
live.promptCycle += 1;
|
|
@@ -2149,7 +2232,8 @@ export function createAssistantRuntime({
|
|
|
2149
2232
|
live.record.lastActiveAt = iso(now());
|
|
2150
2233
|
live.turnStartedMs = now();
|
|
2151
2234
|
live.record.turns = (Number(live.record.turns) || 0) + 1;
|
|
2152
|
-
|
|
2235
|
+
// A name the user gave (updateSession) is never replaced, whatever it says.
|
|
2236
|
+
if (!live.record.userRenamed && isDefaultSessionTitle(live.record.title)) {
|
|
2153
2237
|
if (!system) { live.record.title = clip(prompt.split('\n')[0], 60); broadcast('assistant:session-updated', live); }
|
|
2154
2238
|
}
|
|
2155
2239
|
touch(live);
|
|
@@ -2157,12 +2241,48 @@ export function createAssistantRuntime({
|
|
|
2157
2241
|
// the record, and routed/continuation turns override it for one turn only.
|
|
2158
2242
|
const effectiveModel = turnModel || live.record.brain.model || null;
|
|
2159
2243
|
const effectiveEffort = brainEffort(live.record.brain.provider, effectiveModel, turnEffort || live.record.brain.effort || null);
|
|
2244
|
+
// User context (assistant-user-context.js): a human turn's trace and learning start here.
|
|
2245
|
+
// A Codex / OpenCode turn gets its per-turn block now (its recall result is known); a Claude
|
|
2246
|
+
// turn gets it in the in-process UserPromptSubmit hook, which finds the handle in turnMeta.
|
|
2247
|
+
let ucTurn = null;
|
|
2248
|
+
// Greeting (lib/greeting/): a human turn is the person being here, and the greeting the panel
|
|
2249
|
+
// showed when this conversation opened is told to the brain once, as background (never for a
|
|
2250
|
+
// slash command). The user-context block carries it when that engine runs; else it goes in
|
|
2251
|
+
// front of the user's words by itself.
|
|
2252
|
+
let greeted = null;
|
|
2253
|
+
if (!system && greeting) {
|
|
2254
|
+
try { greeting.noteActivity({ project: live.record.project || null, cwd: live.record.brain.cwd || null, surface: 'assistant', words: typed || prompt }); } catch { /* the turn goes on */ }
|
|
2255
|
+
if (greetingId) {
|
|
2256
|
+
try {
|
|
2257
|
+
const shown = greeting.peek(greetingId);
|
|
2258
|
+
const note = greeting.consumeNote(greetingId, { skip: prompt.trimStart().startsWith('/') });
|
|
2259
|
+
if (note && shown) greeted = { id: shown.id, text: shown.text, note, ids: (shown.basis || []).map((row) => row.id).filter(Boolean), source: shown.source, model: shown.model, costUsd: shown.costUsd };
|
|
2260
|
+
} catch { greeted = null; }
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
if (!system && userContext) {
|
|
2264
|
+
try {
|
|
2265
|
+
ucTurn = userContext.beginTurn(live.record, {
|
|
2266
|
+
prompt, typed, origin: origin || null, untrusted: untrusted === true, generation: live.record.memoryGeneration || 1,
|
|
2267
|
+
previousReply: live.lastReply || '', cwd: live.record.brain.cwd || null, taskId, greeting: greeted,
|
|
2268
|
+
emit: (event) => emitAutomation(live, event), persist: () => touch(live), openCards: () => openCards(live),
|
|
2269
|
+
});
|
|
2270
|
+
if (live.brainProvider !== 'claude-code') {
|
|
2271
|
+
const block = userContext.turnBlock(live.record, ucTurn, turnRecalled).text;
|
|
2272
|
+
// After the memories and the router / test lines, right before the user's words.
|
|
2273
|
+
if (block) finalText = finalText.endsWith(prompt) ? `${finalText.slice(0, finalText.length - prompt.length)}${block}\n\n${prompt}` : `${block}\n\n${finalText}`;
|
|
2274
|
+
}
|
|
2275
|
+
live.ucTurns.push(ucTurn);
|
|
2276
|
+
} catch (error) { ucTurn = null; log('assistant:user-context', error?.message || String(error)); }
|
|
2277
|
+
}
|
|
2278
|
+
// No user-context engine to carry it: the note goes right before the user's words.
|
|
2279
|
+
if (greeted && !ucTurn) finalText = finalText.endsWith(prompt) ? `${finalText.slice(0, finalText.length - prompt.length)}${greeted.note}\n\n${prompt}` : `${greeted.note}\n\n${finalText}`;
|
|
2160
2280
|
// The Claude brain recalls in its UserPromptSubmit hook, which sees only the
|
|
2161
2281
|
// final text: record who wrote it (mailbox / router = system) and the user's
|
|
2162
2282
|
// own words, so a router-stamped first turn recalls on the prompt itself.
|
|
2163
2283
|
if (live.brainProvider === 'claude-code' && finalText.trim()) {
|
|
2164
2284
|
live.turnMeta.delete(finalText.trim());
|
|
2165
|
-
live.turnMeta.set(finalText.trim(), { system: !!system, prompt: system ? null : prompt });
|
|
2285
|
+
live.turnMeta.set(finalText.trim(), { system: !!system, prompt: system ? null : prompt, ...(ucTurn ? { uc: ucTurn } : {}) });
|
|
2166
2286
|
while (live.turnMeta.size > 8) live.turnMeta.delete(live.turnMeta.keys().next().value);
|
|
2167
2287
|
}
|
|
2168
2288
|
// Every turn starts unrouted; a continuation or a route_decided "here" turn starts open.
|
|
@@ -2181,6 +2301,12 @@ export function createAssistantRuntime({
|
|
|
2181
2301
|
scheduleUsage(live.record.id);
|
|
2182
2302
|
live.planTurn = false;
|
|
2183
2303
|
live.record.status = 'idle';
|
|
2304
|
+
// The prompt never reached the brain: nothing is traced or learned from it.
|
|
2305
|
+
if (ucTurn) {
|
|
2306
|
+
const at = live.ucTurns.indexOf(ucTurn);
|
|
2307
|
+
if (at >= 0) live.ucTurns.splice(at, 1);
|
|
2308
|
+
try { userContext?.cancelTurn(live.record, ucTurn); } catch {}
|
|
2309
|
+
}
|
|
2184
2310
|
const message = error?.message || String(error);
|
|
2185
2311
|
sendToSockets(live, { type: 'error', message });
|
|
2186
2312
|
sendToSockets(live, { type: 'done', code: 1 });
|
|
@@ -2281,6 +2407,8 @@ export function createAssistantRuntime({
|
|
|
2281
2407
|
if (!run?.assistantSessionId) return;
|
|
2282
2408
|
const live = sessions.get(run.assistantSessionId);
|
|
2283
2409
|
if (!live) return;
|
|
2410
|
+
// The conversation's thread record: its runs, their outcomes and follow-ups.
|
|
2411
|
+
try { userContext?.onDispatch(live.record, payload); } catch {}
|
|
2284
2412
|
if (!live.record.dispatchRunIds?.includes(run.runId)) { live.record.dispatchRunIds = [...(live.record.dispatchRunIds || []), run.runId].slice(-100); touch(live); }
|
|
2285
2413
|
sendToSockets(live, { type: 'assistant:dispatch', reason: payload.reason, run });
|
|
2286
2414
|
scheduleUsage(live.record.id);
|
|
@@ -2316,6 +2444,7 @@ export function createAssistantRuntime({
|
|
|
2316
2444
|
if (!live) return;
|
|
2317
2445
|
const removed = new Set((payload.runIds || []).map(String));
|
|
2318
2446
|
if (!removed.size) return;
|
|
2447
|
+
try { userContext?.onDispatch(live.record, { type: 'assistant:run-removed', runIds: [...removed] }); } catch {}
|
|
2319
2448
|
for (const runId of removed) {
|
|
2320
2449
|
live.obligations.delete(runId);
|
|
2321
2450
|
live.nudgedRuns.delete(runId);
|
|
@@ -2395,6 +2524,10 @@ export function createAssistantRuntime({
|
|
|
2395
2524
|
if (mode && !normalized) { const error = new Error(`Unknown route mode ${mode}`); error.code = 'ROUTE_MODE_INVALID'; error.status = 400; throw error; }
|
|
2396
2525
|
live.record.routing = { ...(live.record.routing || {}), mode: normalized };
|
|
2397
2526
|
}
|
|
2527
|
+
/** null, '' or an unknown value clears the session's own level: it reads as the default. */
|
|
2528
|
+
function applyTestDensity(live, level) {
|
|
2529
|
+
live.record.testDensity = normalizeTestDensity(level, null);
|
|
2530
|
+
}
|
|
2398
2531
|
function applyComputerUse(live, enabled) {
|
|
2399
2532
|
// A remote session's computer use is not a per-conversation choice: the owner's switch and
|
|
2400
2533
|
// the level decide (remoteComputerUse). A PATCH, the socket and updateSession change nothing.
|
|
@@ -2406,9 +2539,17 @@ export function createAssistantRuntime({
|
|
|
2406
2539
|
async function updateSession(id, patch = {}) {
|
|
2407
2540
|
const live = sessions.get(id);
|
|
2408
2541
|
if (!live) return null;
|
|
2409
|
-
|
|
2542
|
+
// A rename (tab, float header, sidepanel ✎) is the session's one name: every list reads it
|
|
2543
|
+
// here, and `userRenamed` tells a host to reopen the session under it. Calling it "SynaBun"
|
|
2544
|
+
// again gives the name up: the next prompt titles it, as for a session never renamed. The
|
|
2545
|
+
// mark stays, as `false`: a host holding the old name with a saved tab must not write it back
|
|
2546
|
+
// (no mark at all means a rename the record never heard of, which a host does write).
|
|
2547
|
+
const renamed = typeof patch.title === 'string' ? clip(patch.title, 60) : '';
|
|
2548
|
+
if (renamed && renamed !== DEFAULT_SESSION_TITLE) { live.record.title = renamed; live.record.userRenamed = true; }
|
|
2549
|
+
else if (renamed && live.record.userRenamed) { live.record.title = DEFAULT_SESSION_TITLE; live.record.userRenamed = false; }
|
|
2410
2550
|
if (patch.routing && typeof patch.routing === 'object' && 'mode' in patch.routing) applyRouteMode(live, patch.routing.mode);
|
|
2411
2551
|
if ('computerUse' in patch) applyComputerUse(live, patch.computerUse);
|
|
2552
|
+
if ('testDensity' in patch) applyTestDensity(live, patch.testDensity);
|
|
2412
2553
|
if (patch.brain && typeof patch.brain === 'object') {
|
|
2413
2554
|
// A provider switch that names no model starts from the new provider's
|
|
2414
2555
|
// default, never from the old provider's model id.
|
|
@@ -2424,11 +2565,19 @@ export function createAssistantRuntime({
|
|
|
2424
2565
|
// A usage limit is the account's: another provider or account starts without the notice.
|
|
2425
2566
|
if (next.provider !== live.record.brain.provider || next.accountId !== live.record.brain.accountId) setLimit(live, null);
|
|
2426
2567
|
live.record.brain = next;
|
|
2427
|
-
|
|
2568
|
+
// The panel's brain names its working folder `project` and never sends `cwd`. A project picked
|
|
2569
|
+
// there (the toolbar's Working project, the empty state's picker) moves the folder the way a
|
|
2570
|
+
// prompt's cwd does (runTurn): a brain that is running keeps its conversation and gets the folder
|
|
2571
|
+
// with its next turn; one not started yet starts there, with it as its Default project.
|
|
2572
|
+
const folder = !patch.brain.cwd && typeof patch.brain.project === 'string' ? patch.brain.project.trim() : '';
|
|
2573
|
+
if (folder && folder !== next.cwd) live.record.brain = { ...next, cwd: folder };
|
|
2574
|
+
live.record.project = detectProject(live.record.brain.cwd) || live.record.project || 'global';
|
|
2428
2575
|
if (providerChanged) {
|
|
2429
2576
|
await disposeBrain(live, 'switched');
|
|
2430
2577
|
live.record.providerSessionId = null;
|
|
2431
2578
|
live.record.providerThreadId = null;
|
|
2579
|
+
// Another conversation: its user-context digest is built (and frozen) anew.
|
|
2580
|
+
try { userContext?.conversationReplaced(live.record); } catch {}
|
|
2432
2581
|
if (live.record.usageState) delete live.record.usageState[next.provider === 'claude-code' ? 'claude' : next.provider];
|
|
2433
2582
|
if (live.record.usageState) delete live.record.usageState.codexThreadId;
|
|
2434
2583
|
live.record.status = 'idle';
|
|
@@ -2472,6 +2621,8 @@ export function createAssistantRuntime({
|
|
|
2472
2621
|
live.record.status = 'ended';
|
|
2473
2622
|
touch(live);
|
|
2474
2623
|
persist({ force: true });
|
|
2624
|
+
// The conversation's thread memory is written now (not awaited: an embedding and a write).
|
|
2625
|
+
try { userContext?.sessionClosed(id); } catch {}
|
|
2475
2626
|
broadcast('assistant:session-ended', live);
|
|
2476
2627
|
} finally { closed(); }
|
|
2477
2628
|
return view(live);
|
|
@@ -2489,6 +2640,7 @@ export function createAssistantRuntime({
|
|
|
2489
2640
|
usageTimers.delete(id);
|
|
2490
2641
|
usageSentAt.delete(id);
|
|
2491
2642
|
metered('drop', () => usage?.dropSession?.(id));
|
|
2643
|
+
try { userContext?.sessionDestroyed(id); } catch {}
|
|
2492
2644
|
try { remotePolicy?.forgetSession?.(id); } catch {}
|
|
2493
2645
|
try { rmSync(resolve(dataDir, 'assistant', id), { recursive: true, force: true }); } catch {}
|
|
2494
2646
|
persist({ force: true });
|
|
@@ -2772,7 +2924,7 @@ export function createAssistantRuntime({
|
|
|
2772
2924
|
// `request_id`: the panel's id for this prompt. A refusal reaches this socket
|
|
2773
2925
|
// alone and carries it back, so this panel (only) puts the text back.
|
|
2774
2926
|
const request = { id: msg.request_id ? String(msg.request_id) : null, reply };
|
|
2775
|
-
const result = await runQuery(live, { text: msg.prompt, images: msg.images || [], cwd: msg.cwd || null, model: msg.model || null, effort: msg.effort || null, permissionMode: msg.permissionMode || null, planMode: typeof msg.planMode === 'boolean' ? msg.planMode : null, origin: 'ui', request });
|
|
2927
|
+
const result = await runQuery(live, { text: msg.prompt, images: msg.images || [], cwd: msg.cwd || null, model: msg.model || null, effort: msg.effort || null, permissionMode: msg.permissionMode || null, planMode: typeof msg.planMode === 'boolean' ? msg.planMode : null, origin: 'ui', request, typed: typeof msg.typed === 'string' ? msg.typed : null, greetingId: typeof msg.greetingId === 'string' ? msg.greetingId : null });
|
|
2776
2928
|
// Refused before it ran: the panel, "working" since it sent, shows what runs now.
|
|
2777
2929
|
if (result && !result.ok && result.code !== 'TURN_FAILED') reply({ type: 'reattach_result', ok: true, sessionId: live.record.id, running: isBusy(live.record.id) });
|
|
2778
2930
|
return;
|
|
@@ -2814,6 +2966,12 @@ export function createAssistantRuntime({
|
|
|
2814
2966
|
broadcast('assistant:session-updated', live);
|
|
2815
2967
|
sendToSockets(live, { type: 'assistant:session', session: view(live), reason: 'routing' });
|
|
2816
2968
|
return;
|
|
2969
|
+
case 'set_test_density':
|
|
2970
|
+
applyTestDensity(live, msg.level);
|
|
2971
|
+
touch(live);
|
|
2972
|
+
broadcast('assistant:session-updated', live);
|
|
2973
|
+
sendToSockets(live, { type: 'assistant:session', session: view(live), reason: 'tests' });
|
|
2974
|
+
return;
|
|
2817
2975
|
case 'set_computer_use':
|
|
2818
2976
|
applyComputerUse(live, msg.enabled);
|
|
2819
2977
|
touch(live);
|
|
@@ -2837,12 +2995,14 @@ export function createAssistantRuntime({
|
|
|
2837
2995
|
return;
|
|
2838
2996
|
case 'compact':
|
|
2839
2997
|
live.record.memoryGeneration = (live.record.memoryGeneration || 1) + 1;
|
|
2998
|
+
try { userContext?.onCompaction(live.record); } catch {}
|
|
2840
2999
|
touch(live);
|
|
2841
3000
|
// Session work: it starts once a running turn ends, and nothing else starts until it settles.
|
|
2842
3001
|
queueWork(live, { kind: 'compact', reply });
|
|
2843
3002
|
return;
|
|
2844
3003
|
case 'clear':
|
|
2845
3004
|
live.record.memoryGeneration = (live.record.memoryGeneration || 1) + 1;
|
|
3005
|
+
try { userContext?.onCompaction(live.record); } catch {}
|
|
2846
3006
|
touch(live);
|
|
2847
3007
|
return;
|
|
2848
3008
|
case 'heartbeat':
|
|
@@ -2869,6 +3029,8 @@ export function createAssistantRuntime({
|
|
|
2869
3029
|
live.record.providerSessionId = msg.providerSessionId;
|
|
2870
3030
|
// Another provider session: the meter's baseline was the old one's.
|
|
2871
3031
|
if (moved) resetMeterIdentity(live, msg.providerSessionId);
|
|
3032
|
+
// …and so was the frozen user-context digest.
|
|
3033
|
+
if (moved) { try { userContext?.conversationReplaced(live.record); } catch {} }
|
|
2872
3034
|
touch(live);
|
|
2873
3035
|
await live.brain?.resume?.(msg.providerSessionId);
|
|
2874
3036
|
}
|
|
@@ -2887,6 +3049,12 @@ export function createAssistantRuntime({
|
|
|
2887
3049
|
}
|
|
2888
3050
|
}
|
|
2889
3051
|
|
|
3052
|
+
/** The dispatcher's reading of a session's Test density (wired in server.js); null for an unknown session. */
|
|
3053
|
+
function testDensityFor(id) {
|
|
3054
|
+
const live = sessions.get(id);
|
|
3055
|
+
return live ? testDensityOf(live.record) : null;
|
|
3056
|
+
}
|
|
3057
|
+
|
|
2890
3058
|
// ── router sinks (wired lazily in server.js; see assistant-router.js) ──────
|
|
2891
3059
|
function routerSession(id) {
|
|
2892
3060
|
const live = sessions.get(id);
|
|
@@ -3303,6 +3471,7 @@ export function createAssistantRuntime({
|
|
|
3303
3471
|
live.record.status = 'idle';
|
|
3304
3472
|
}
|
|
3305
3473
|
persist({ force: true });
|
|
3474
|
+
try { userContext?.shutdown(); } catch {}
|
|
3306
3475
|
// Rows whose append failed get one more try before the process goes.
|
|
3307
3476
|
metered('flush', () => usage?.flush?.());
|
|
3308
3477
|
}
|
|
@@ -3326,12 +3495,12 @@ export function createAssistantRuntime({
|
|
|
3326
3495
|
handleWebSocket, listSessions, getSession, createSession, updateSession, closeSession, destroySession,
|
|
3327
3496
|
resolveTerminal, accountInUse, ownsIsolatedServe, noteDispatch, subscribe, shutdown,
|
|
3328
3497
|
observeBroadcast: onPermissionBroadcast,
|
|
3329
|
-
routerSession, routerSend, routerEvent, routerMailbox, routerContinue, routerSinks, routerRouted, routerRouteFailed, routerComputerApproved, gateCheck, isPlanning, isPlanReadOnly,
|
|
3498
|
+
testDensityFor, routerSession, routerSend, routerEvent, routerMailbox, routerContinue, routerSinks, routerRouted, routerRouteFailed, routerComputerApproved, gateCheck, isPlanning, isPlanReadOnly,
|
|
3330
3499
|
clarifySession, clarifyEvent, clarifyGate, clarifySinks,
|
|
3331
3500
|
getComputerUse, getComputerTurn, supersedeComputerApproval, abortTurn, notifySession,
|
|
3332
3501
|
brainSpend, brainCapUsd, onBudgetChanged, currentTask, usageView,
|
|
3333
3502
|
// Programmatic surface (the WhatsApp bridge) and remote sessions.
|
|
3334
3503
|
observe, submit, stopTurn, answerControl, answerDispatchControl, pendingControls, isBusy, sessionPolicy, sessionChannel,
|
|
3335
|
-
_internals: { sessions, ensureBrain, disposeBrain, runQuery, enqueueMailbox, deliverMailbox, settings, ensureGate, onBrainPacket, onTurnFinished, remoteHooks, packetObservers },
|
|
3504
|
+
_internals: { sessions, ensureBrain, disposeBrain, runQuery, enqueueMailbox, deliverMailbox, settings, ensureGate, onBrainPacket, onTurnFinished, remoteHooks, packetObservers, userContext },
|
|
3336
3505
|
};
|
|
3337
3506
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// parses. Pure module: no I/O, no server imports.
|
|
11
11
|
|
|
12
12
|
import { browserPolicyText } from './browser-tool-policy.js';
|
|
13
|
+
import { testingBlock, testingFollowUpLine } from './assistant-test-density.js';
|
|
13
14
|
|
|
14
15
|
export const RESULT_HEADING = '## Result';
|
|
15
16
|
/** What a worker without a browser says to get one (the brain dispatches it again with uses_browser). */
|
|
@@ -291,7 +292,9 @@ export function briefBlock(brief) {
|
|
|
291
292
|
* collectMedia ('image' | 'video' for Image / Video creation runs; 'image' for design),
|
|
292
293
|
* playbook ({ name, text } for design runs: assistant-playbooks.js),
|
|
293
294
|
* styleGuide ({ summary, designPath, tokenFiles, proposals } when the project has a saved Style Guide
|
|
294
|
-
* that lets this class of run in: lib/style-guide/store.js loadStyleGuideForRun)
|
|
295
|
+
* that lets this class of run in: lib/style-guide/store.js loadStyleGuideForRun),
|
|
296
|
+
* testDensity (the session's Test density when the dispatcher composes the prompt; the TESTING block,
|
|
297
|
+
* for code / complex / quick runs and runs with no class: assistant-test-density.js) }
|
|
295
298
|
*/
|
|
296
299
|
export function buildTaskPrompt(state = {}) {
|
|
297
300
|
const task = clean(state.task);
|
|
@@ -335,12 +338,18 @@ export function buildTaskPrompt(state = {}) {
|
|
|
335
338
|
if (brief) lines.push('', brief);
|
|
336
339
|
const context = clean(state.context);
|
|
337
340
|
if (context) lines.push('', 'CONTEXT (from the assistant):', context);
|
|
341
|
+
// After TASK and CONTEXT, which it overrides: the only place SynaBun tells a worker how much to test.
|
|
342
|
+
const testing = testingBlock(state.testDensity, state.taskClass);
|
|
343
|
+
if (testing) lines.push('', testing);
|
|
338
344
|
lines.push('', resultContractText({ requireJson: !!state.outputSchema, media: !!(mediaBlock(state) || designBlock(state)) }));
|
|
339
345
|
return lines.filter((line) => line !== null && line !== undefined).join('\n').replace(/\n{3,}/g, '\n\n');
|
|
340
346
|
}
|
|
341
347
|
|
|
342
|
-
/**
|
|
343
|
-
|
|
348
|
+
/**
|
|
349
|
+
* Follow-up turn wrapper (agent_send / user steer) for the same run.
|
|
350
|
+
* `testDensity`: the level, only when it changed since the run's last TESTING rules.
|
|
351
|
+
*/
|
|
352
|
+
export function buildFollowUpPrompt(text, state = {}, { turn = null, maxTurns = null, origin = 'assistant', testDensity = null } = {}) {
|
|
344
353
|
const project = clean(state.project) || 'global';
|
|
345
354
|
const runId = clean(state.runId) || 'unknown-run';
|
|
346
355
|
const counter = turn && maxTurns ? ` ${turn}/${maxTurns}` : turn ? ` ${turn}` : '';
|
|
@@ -349,6 +358,7 @@ export function buildFollowUpPrompt(text, state = {}, { turn = null, maxTurns =
|
|
|
349
358
|
`${who}${counter} (same run ${runId}, project "${project}"):`,
|
|
350
359
|
clean(text),
|
|
351
360
|
'',
|
|
361
|
+
...(testDensity ? [testingFollowUpLine(testDensity), ''] : []),
|
|
352
362
|
`Apply the same MEMORY and OUTPUT CONTRACT rules; end with the ${RESULT_HEADING} block.`,
|
|
353
363
|
].join('\n');
|
|
354
364
|
}
|