@loicngr/kobo 1.10.3 → 1.11.1
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/AGENTS.md +5 -4
- package/CHANGELOG.md +31 -0
- package/README.md +16 -10
- package/dist/mcp-server/kobo-tasks-server.js +1 -1
- package/dist/server/db/migrations.js +21 -0
- package/dist/server/db/schema.js +14 -0
- package/dist/server/routes/dev-server.js +14 -0
- package/dist/server/routes/health.js +2 -2
- package/dist/server/routes/notion.js +18 -1
- package/dist/server/routes/sentry.js +15 -1
- package/dist/server/routes/workspaces.js +221 -6
- package/dist/server/services/agent/engines/claude-code/engine.js +17 -1
- package/dist/server/services/agent/engines/claude-code/event-mapper.js +31 -2
- package/dist/server/services/agent/engines/claude-code/options-builder.js +3 -0
- package/dist/server/services/agent/engines/codex/engine.js +70 -8
- package/dist/server/services/agent/engines/codex/server-requests.js +13 -3
- package/dist/server/services/agent/engines/codex/turn-liveness.js +53 -0
- package/dist/server/services/agent/event-router.js +4 -0
- package/dist/server/services/agent/orchestrator.js +17 -0
- package/dist/server/services/agent/streaming-batcher.js +34 -0
- package/dist/server/services/forge/github/provider.js +4 -1
- package/dist/server/services/forge/gitlab/provider.js +4 -1
- package/dist/server/services/forge/none.js +4 -1
- package/dist/server/services/notion-service.js +19 -0
- package/dist/server/services/search-service.js +3 -1
- package/dist/server/services/sentry-service.js +20 -0
- package/dist/server/services/settings-service.js +24 -0
- package/dist/server/services/workspace-permission-policy-service.js +86 -0
- package/dist/server/utils/paths.js +3 -3
- package/package.json +3 -3
- package/src/client/dist/pwa/assets/ActivityFeed-ByJjMo0k.js +8 -0
- package/src/client/dist/pwa/assets/ActivityFeed-D9EfbRqv.css +1 -0
- package/src/client/dist/{spa/assets/ChangelogPage-OyS0LqjP.js → pwa/assets/ChangelogPage-CppcIG5K.js} +1 -1
- package/src/client/dist/{spa/assets/ClosePopup-B1wf7Gh3.js → pwa/assets/ClosePopup-B0-qi7Jo.js} +1 -1
- package/src/client/dist/pwa/assets/CreatePage-D0xvdJAQ.js +2 -0
- package/src/client/dist/pwa/assets/CreatePage-qMgdzdb-.css +1 -0
- package/src/client/dist/pwa/assets/DiffViewer-COD5Mdp_.js +8 -0
- package/src/client/dist/{spa/assets/DiffViewer-BuYD1EDP.css → pwa/assets/DiffViewer-CRrDdYBa.css} +1 -1
- package/src/client/dist/{spa/assets/HealthPage-QMan-G3D.js → pwa/assets/HealthPage-Bnad2BVm.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-Bwulksar.css +1 -0
- package/src/client/dist/pwa/assets/MainLayout-nn4YIcy8.js +37 -0
- package/src/client/dist/{spa/assets/QBtnToggle-BKi1Jbys.js → pwa/assets/QBtnToggle-CZXgD3Hf.js} +1 -1
- package/src/client/dist/{spa/assets/QChip-1uTELmCE.js → pwa/assets/QChip-BJH-iJCV.js} +1 -1
- package/src/client/dist/{spa/assets/QSelect-CKc3IVuT.js → pwa/assets/QSelect-tsFTetLi.js} +1 -1
- package/src/client/dist/{spa/assets/QTooltip-86biX27D.js → pwa/assets/QTooltip-CA_Iyybc.js} +1 -1
- package/src/client/dist/pwa/assets/SearchPage-B08_UXDs.css +1 -0
- package/src/client/dist/pwa/assets/SearchPage-BwD6K8qD.js +1 -0
- package/src/client/dist/pwa/assets/SettingsPage-CF-xI5PO.js +16 -0
- package/src/client/dist/{spa/assets/SettingsPage-DGAGBtQ2.css → pwa/assets/SettingsPage-DJEH2ahl.css} +1 -1
- package/src/client/dist/pwa/assets/ThinkingItem-Ckk4K1O-.css +1 -0
- package/src/client/dist/pwa/assets/ThinkingItem-DBWuhjgW.js +1 -0
- package/src/client/dist/{spa/assets/TouchPan-EYoNwghH.js → pwa/assets/TouchPan-txugr_lm.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-Cv04uh_0.css +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-DgucXHjd.js +4 -0
- package/src/client/dist/{spa/assets/build-path-tree-BAxSTjwb.js → pwa/assets/build-path-tree-BUkEDxlT.js} +1 -1
- package/src/client/dist/{spa/assets/cssMode-CG3m7iQl.js → pwa/assets/cssMode-BDLIXvw9.js} +1 -1
- package/src/client/dist/{spa/assets/documents-Dz8sHeyR.js → pwa/assets/documents-ClS4k_bT.js} +1 -1
- package/src/client/dist/{spa/assets/editor.api2-DqJsxy06.js → pwa/assets/editor.api2-PhaZVUXg.js} +1 -1
- package/src/client/dist/{spa/assets/editor.main-r2oDGbHA.js → pwa/assets/editor.main-2nNvSQc1.js} +2 -2
- package/src/client/dist/{spa/assets/expand-template-DP5gf1ro.js → pwa/assets/expand-template-HgUyAUBK.js} +1 -1
- package/src/client/dist/{spa/assets/formatters-EQwGVQQS.js → pwa/assets/formatters-Chk4XGyr.js} +1 -1
- package/src/client/dist/{spa/assets/freemarker2-C6Dcsusp.js → pwa/assets/freemarker2-Dy02xxKp.js} +1 -1
- package/src/client/dist/{spa/assets/handlebars-BYzVrRyn.js → pwa/assets/handlebars-Do59Qec5.js} +1 -1
- package/src/client/dist/{spa/assets/html-Yuf2QU9u.js → pwa/assets/html-MJ9-LLjS.js} +1 -1
- package/src/client/dist/{spa/assets/htmlMode-BJJsbkSn.js → pwa/assets/htmlMode-AsZ-SMzZ.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-Djv0PzK4.js +1 -0
- package/src/client/dist/pwa/assets/index-BpTtR9lB.js +84 -0
- package/src/client/dist/{spa/assets/javascript-Ch3ueiOd.js → pwa/assets/javascript-DoZFpstd.js} +1 -1
- package/src/client/dist/{spa/assets/jsonMode-xEENY1ms.js → pwa/assets/jsonMode-CMeFz2Y2.js} +1 -1
- package/src/client/dist/{spa/assets/kobo-commands-CbIH5Wgu.js → pwa/assets/kobo-commands-DmJq1I6k.js} +1 -1
- package/src/client/dist/{spa/assets/layout-EwsT5w-M.js → pwa/assets/layout-BSJkgTMQ.js} +1 -1
- package/src/client/dist/{spa/assets/liquid-Bh89yiMp.js → pwa/assets/liquid-Bc70ZdlV.js} +1 -1
- package/src/client/dist/{spa/assets/lspLanguageFeatures-CNi35cTh.js → pwa/assets/lspLanguageFeatures-Co4WnPYT.js} +1 -1
- package/src/client/dist/{spa/assets/mdx-Pk_6m6B2.js → pwa/assets/mdx-BsXN2I7z.js} +1 -1
- package/src/client/dist/{spa/assets/monaco.contribution-DyjS5cak.js → pwa/assets/monaco.contribution-BpmQVvpQ.js} +2 -2
- package/src/client/dist/{spa/assets/network-auth-B4eYLa1I.js → pwa/assets/network-auth-B0RCBQ5K.js} +1 -1
- package/src/client/dist/{spa/assets/QScrollArea-ehpkh2GE.js → pwa/assets/pending-diff-open-BiozufWe.js} +1 -1
- package/src/client/dist/{spa/assets/QExpansionItem-BJZyoT79.js → pwa/assets/permissionModes-kRWgzgvi.js} +1 -1
- package/src/client/dist/{spa/assets/project-color-8ANVbJax.js → pwa/assets/project-color-D35ZMh9_.js} +1 -1
- package/src/client/dist/{spa/assets/python-7GmD1T0-.js → pwa/assets/python-CV5Zt8gJ.js} +1 -1
- package/src/client/dist/{spa/assets/razor-CTW-7qAs.js → pwa/assets/razor-4Ev2x3ig.js} +1 -1
- package/src/client/dist/{spa/assets/render-chat-markdown-iOBona09.js → pwa/assets/render-chat-markdown-Cjt11WnX.js} +1 -1
- package/src/client/dist/{spa/assets/tsMode-BMCTiDkj.js → pwa/assets/tsMode-BDysTsn1.js} +1 -1
- package/src/client/dist/{spa/assets/typescript-CBNlJ2bB.js → pwa/assets/typescript-DM1CefPN.js} +1 -1
- package/src/client/dist/{spa/assets/use-onboarding-C7pg8RV8.js → pwa/assets/use-onboarding-D2BGKiP_.js} +1 -1
- package/src/client/dist/{spa/assets/use-quasar-by40ecAs.js → pwa/assets/use-quasar-Cps11O9Y.js} +1 -1
- package/src/client/dist/{spa/assets/workers-CrZaPx7e.js → pwa/assets/workers-BuJZJ6j6.js} +1 -1
- package/src/client/dist/{spa/assets/xml-CkKKuUBJ.js → pwa/assets/xml-DzARVQ9o.js} +1 -1
- package/src/client/dist/{spa/assets/yaml-B6VtPgSa.js → pwa/assets/yaml-P0ZYSZvj.js} +1 -1
- package/src/client/dist/{spa → pwa}/index.html +2 -2
- package/src/client/dist/pwa/manifest.json +1 -0
- package/src/client/dist/pwa/sw.js +1 -0
- package/src/client/dist/pwa/workbox-778a14ff.js +1 -0
- package/src/mcp-server/kobo-tasks-server.ts +1 -1
- package/src/client/dist/spa/assets/ActivityFeed-Bn4IG1gM.js +0 -8
- package/src/client/dist/spa/assets/ActivityFeed-HbMZOClq.css +0 -1
- package/src/client/dist/spa/assets/CreatePage-C1yAhjsO.css +0 -1
- package/src/client/dist/spa/assets/CreatePage-CkOwo6Jn.js +0 -2
- package/src/client/dist/spa/assets/DiffViewer-DUy5tfI3.js +0 -8
- package/src/client/dist/spa/assets/MainLayout-7nwhu8UE.css +0 -1
- package/src/client/dist/spa/assets/MainLayout-CkVcmDeg.js +0 -37
- package/src/client/dist/spa/assets/SearchPage-V8OnALqt.js +0 -1
- package/src/client/dist/spa/assets/SearchPage-cVwt0DaQ.css +0 -1
- package/src/client/dist/spa/assets/SettingsPage-CazPsJnl.js +0 -16
- package/src/client/dist/spa/assets/WorkspacePage-BOApCawq.css +0 -1
- package/src/client/dist/spa/assets/WorkspacePage-q6R6SPuh.js +0 -4
- package/src/client/dist/spa/assets/i18n-CPaxvFKM.js +0 -1
- package/src/client/dist/spa/assets/index-CC1iOXc-.js +0 -84
- package/src/client/dist/spa/assets/permissionModes-6jhegAm-.js +0 -1
- /package/src/client/dist/{spa → pwa}/assets/ChangelogPage-CQ33An2f.css +0 -0
- /package/src/client/dist/{spa → pwa}/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuYjalmUiAw-BCOjigVR.woff +0 -0
- /package/src/client/dist/{spa → pwa}/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuZtalmUiAw-Ocp2tNig.woff +0 -0
- /package/src/client/dist/{spa → pwa}/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuaabVmUiAw-6jEn9xEl.woff +0 -0
- /package/src/client/dist/{spa → pwa}/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWub2bVmUiAw-CCs8I9zJ.woff +0 -0
- /package/src/client/dist/{spa → pwa}/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbFmUiAw-BLaboPi0.woff +0 -0
- /package/src/client/dist/{spa → pwa}/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAw-DHuK6FBl.woff +0 -0
- /package/src/client/dist/{spa → pwa}/assets/QBadge-DV5kn4mL.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/QBanner-CmfaeizG.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/QBtnGroup-C442-yBb.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/QIcon-C6E-oEjI.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/QInput-BF8x4RTv.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/QList-D5EKdFPn.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/QPage-D3rjwA1l.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/QSeparator-DWTCQxsa.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/QSpace-ezNrZkuj.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/QSpinnerDots-DIV68-cV.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/_plugin-vue_export-helper-BDNMzG2s.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/abap-08VXUWAP.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/apex-BWPQTe0t.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/azcli-Bc_sGQ0U.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/bat-i0X4ZdIN.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/bicep-B5-_aFwp.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/cameligo-DMUM7wLl.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/chunk-QTnfLwEv.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/clojure-Cm7r79vr.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/codicon-ngg6Pgfi.ttf +0 -0
- /package/src/client/dist/{spa → pwa}/assets/coffee-Ba7i2nA0.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/cpp-C7h46wYY.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/csharp-BKxtCVv1.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/csp-bTuwJoIa.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/css-DIMkf-bt.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/css.worker-CBlhdqTa.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/cypher-CVaqCwHa.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/dart-onAF5SnQ.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/dockerfile-DZFCIeNp.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/ecl-D05T4iGw.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/editor-BFebQ78e.css +0 -0
- /package/src/client/dist/{spa → pwa}/assets/editor.worker-Cu3tR8iJ.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/elixir-6RTg0lbw.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/expand-template-D2yUa54D.css +0 -0
- /package/src/client/dist/{spa → pwa}/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
- /package/src/client/dist/{spa → pwa}/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ-D-x-0Q06.woff2 +0 -0
- /package/src/client/dist/{spa → pwa}/assets/flow9-C5_-GSwl.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/fsharp-C8Ef5oNN.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/go-C-y9NEjX.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/graphql-fmXr3nnJ.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/hcl-CpzslTdj.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/html.worker-BKBrNna1.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/index-CwYR3iHt.css +0 -0
- /package/src/client/dist/{spa → pwa}/assets/ini-sBoK_t0W.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/java-BEtHBSE6.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/json.worker-BaLYt9Ss.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/julia-Bri6UV-V.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/kotlin-BOotOW0E.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/less-B9JPFI3C.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/lexon-CfSJPG6W.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/lua-CsQS60Ue.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/m3-D-oSqn_W.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/markdown-Cimd5fb3.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/mips-CIPQ_RoX.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/msdax-DauUninz.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/mysql-SOo6toE5.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/objective-c-FvmIjYaQ.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/pascal-DrH0SRf2.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/pascaligo-D-ptJ9y-.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/perl-oz_6vUea.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/pgsql-DTj74zXo.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/php-nr791fC2.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/pla-CopQ2nXW.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/postiats-43DmfD33.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/powerquery-D3hlyOfw.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/powershell-DmHpPYUd.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/protobuf-C531GsRP.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/pug-Z5eAx3Zn.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/qsharp-DkqhCAOL.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/r-BwWrilGY.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/redis-ClamHrr6.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/redshift-DT7zqm-g.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/restructuredtext-BYgofb2h.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/ruby-DezsRK8O.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/rust-DdL9SqIa.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/sb-CcwsVR0C.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/scala-DHpiXF5c.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/scheme-BeGwcela.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/scss-gp-XZpBa.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/shell-CC2rA5mh.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/solidity-BEEn4gHE.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/sophia-CRfGWb83.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/sparql-D_Lu-MrJ.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/sql-NEE52Syq.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/st-DbInun42.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/swift-Bxkupp3x.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/systemverilog-Bz4Y3fRF.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/tcl-DISqw1ZD.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/ts.worker-PmaSgaZk.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/twig-De2hgUGE.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/typespec-B8J7ngcE.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/use-checkbox-E9SV7iH5.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/use-onboarding-B3l7mx48.css +0 -0
- /package/src/client/dist/{spa → pwa}/assets/vb-DV3o63ZY.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/vue.runtime.esm-bundler-JZnIeD9D.js +0 -0
- /package/src/client/dist/{spa → pwa}/assets/wgsl-DpFanUEy.js +0 -0
- /package/src/client/dist/{spa → pwa}/favicon.svg +0 -0
- /package/src/client/dist/{spa → pwa}/sounds/7eme-compagnie-03.mp3 +0 -0
- /package/src/client/dist/{spa → pwa}/sounds/aller-ftg.mp3 +0 -0
- /package/src/client/dist/{spa → pwa}/sounds/arrete-de-mentir.mp3 +0 -0
- /package/src/client/dist/{spa → pwa}/sounds/arretez-les-messages.mp3 +0 -0
- /package/src/client/dist/{spa → pwa}/sounds/bah-alors-on-est-nul.mp3 +0 -0
- /package/src/client/dist/{spa → pwa}/sounds/ca_va_peter.mp3 +0 -0
- /package/src/client/dist/{spa → pwa}/sounds/dry-fart.mp3 +0 -0
- /package/src/client/dist/{spa → pwa}/sounds/faaah.mp3 +0 -0
- /package/src/client/dist/{spa → pwa}/sounds/for-shure.mp3 +0 -0
- /package/src/client/dist/{spa → pwa}/sounds/gta-v-death.mp3 +0 -0
- /package/src/client/dist/{spa → pwa}/sounds/hey.mp3 +0 -0
- /package/src/client/dist/{spa → pwa}/sounds/nan-tu-degages.mp3 +0 -0
- /package/src/client/dist/{spa → pwa}/sounds/nan-wallah-pardon.mp3 +0 -0
- /package/src/client/dist/{spa → pwa}/sounds/ouais-cest-greg.mp3 +0 -0
- /package/src/client/dist/{spa → pwa}/sounds/pas-ca-zinedine.mp3 +0 -0
- /package/src/client/dist/{spa → pwa}/sounds/ta-gueule.mp3 +0 -0
- /package/src/client/dist/{spa → pwa}/sounds/travail_termine.mp3 +0 -0
- /package/src/client/dist/{spa → pwa}/sounds/tu-vas-la-fermer.mp3 +0 -0
- /package/src/client/dist/{spa → pwa}/sounds/warcraft-3-humain-travail.mp3 +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { query, } from '@anthropic-ai/claude-agent-sdk';
|
|
2
2
|
import { nanoid } from 'nanoid';
|
|
3
|
+
import { isWorkspacePermissionAllowed } from '../../../workspace-permission-policy-service.js';
|
|
4
|
+
import { createStreamingBatcher } from '../../streaming-batcher.js';
|
|
3
5
|
import { CLAUDE_CODE_CAPABILITIES } from './capabilities.js';
|
|
4
6
|
import { createMapperState, mapSdkMessage, QUOTA_PATTERN, tryEmitQuota } from './event-mapper.js';
|
|
5
7
|
import { buildClaudeOptions } from './options-builder.js';
|
|
@@ -100,10 +102,17 @@ export function createClaudeCodeEngine() {
|
|
|
100
102
|
if (toolName !== 'AskUserQuestion' && !isInteractive) {
|
|
101
103
|
return Promise.resolve({ behavior: 'allow', updatedInput: input });
|
|
102
104
|
}
|
|
105
|
+
if (toolName !== 'AskUserQuestion' &&
|
|
106
|
+
isWorkspacePermissionAllowed(options.workspaceId, { engine: 'claude-code', toolName, payload: input })) {
|
|
107
|
+
return Promise.resolve({ behavior: 'allow', updatedInput: input });
|
|
108
|
+
}
|
|
103
109
|
const requestKind = toolName === 'AskUserQuestion' ? 'question' : 'permission';
|
|
104
110
|
return new Promise((resolve, reject) => {
|
|
105
111
|
const resolver = { resolve, input, requestKind };
|
|
106
112
|
pendingResolvers.set(toolCallId, resolver);
|
|
113
|
+
// A user decision is intentional inactivity. The text-only watchdog
|
|
114
|
+
// must never end this session while the permission card is visible.
|
|
115
|
+
clearTextIdleWatchdog();
|
|
107
116
|
const onAbort = () => {
|
|
108
117
|
if (pendingResolvers.get(toolCallId) === resolver) {
|
|
109
118
|
pendingResolvers.delete(toolCallId);
|
|
@@ -202,7 +211,7 @@ export function createClaudeCodeEngine() {
|
|
|
202
211
|
let discoveredSessionId;
|
|
203
212
|
// A throwing onEvent handler (e.g. DB query against a closed connection
|
|
204
213
|
// during async test teardown) must not escape as an unhandled rejection.
|
|
205
|
-
const
|
|
214
|
+
const emitDirect = (ev) => {
|
|
206
215
|
try {
|
|
207
216
|
onEvent(ev);
|
|
208
217
|
}
|
|
@@ -210,6 +219,8 @@ export function createClaudeCodeEngine() {
|
|
|
210
219
|
console.error('[claude-engine] onEvent handler threw:', err);
|
|
211
220
|
}
|
|
212
221
|
};
|
|
222
|
+
const streamingBatcher = createStreamingBatcher(emitDirect);
|
|
223
|
+
const safeEmit = (ev) => streamingBatcher.push(ev);
|
|
213
224
|
let iteratorRunning = false;
|
|
214
225
|
let userInterrupted = false;
|
|
215
226
|
let completedResponses = 0;
|
|
@@ -240,6 +251,10 @@ export function createClaudeCodeEngine() {
|
|
|
240
251
|
const armTextIdleWatchdog = () => {
|
|
241
252
|
clearTextIdleWatchdog();
|
|
242
253
|
textIdleTimer = setTimeout(() => {
|
|
254
|
+
if (pendingResolvers.size > 0) {
|
|
255
|
+
textIdleTimer = undefined;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
243
258
|
console.warn(`[claude-engine] SDK stream inactive ${TEXT_IDLE_TIMEOUT_MS}ms after a text-only response — forcing session:ended`);
|
|
244
259
|
const reason = userInterrupted ? 'killed' : mapperState.sawErrorResult ? 'error' : 'completed';
|
|
245
260
|
emitSessionEnded(reason, reason === 'completed' ? 0 : null);
|
|
@@ -322,6 +337,7 @@ export function createClaudeCodeEngine() {
|
|
|
322
337
|
}
|
|
323
338
|
}
|
|
324
339
|
finally {
|
|
340
|
+
streamingBatcher.close();
|
|
325
341
|
// The post-result drain watchdog (if armed) is moot once the
|
|
326
342
|
// iterator has exited — clear it so a healthy run never triggers a
|
|
327
343
|
// stray abort after it already ended.
|
|
@@ -151,6 +151,33 @@ export function mapSdkMessage(msg, state) {
|
|
|
151
151
|
}
|
|
152
152
|
return events;
|
|
153
153
|
}
|
|
154
|
+
if (type === 'stream_event') {
|
|
155
|
+
const event = parsed.event;
|
|
156
|
+
if (!event)
|
|
157
|
+
return events;
|
|
158
|
+
if (event.type === 'message_start') {
|
|
159
|
+
const streamedMessage = event.message;
|
|
160
|
+
const messageId = typeof streamedMessage?.id === 'string' ? streamedMessage.id : undefined;
|
|
161
|
+
if (messageId) {
|
|
162
|
+
state.streamingMessageId = messageId;
|
|
163
|
+
if (!state.openMessages.has(messageId))
|
|
164
|
+
state.openMessages.set(messageId, { sawText: false });
|
|
165
|
+
}
|
|
166
|
+
return events;
|
|
167
|
+
}
|
|
168
|
+
if (event.type === 'content_block_delta') {
|
|
169
|
+
const delta = event.delta;
|
|
170
|
+
const text = delta?.type === 'text_delta' && typeof delta.text === 'string' ? delta.text : undefined;
|
|
171
|
+
if (text && state.streamingMessageId) {
|
|
172
|
+
const messageId = state.streamingMessageId;
|
|
173
|
+
const messageState = state.openMessages.get(messageId);
|
|
174
|
+
if (messageState)
|
|
175
|
+
messageState.sawText = true;
|
|
176
|
+
events.push({ kind: 'message:text', messageId, text, streaming: true });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return events;
|
|
180
|
+
}
|
|
154
181
|
if (type === 'system') {
|
|
155
182
|
if (subtype === 'compact' || subtype === 'compact_boundary') {
|
|
156
183
|
events.push({ kind: 'session:compacted' });
|
|
@@ -233,8 +260,10 @@ export function mapSdkMessage(msg, state) {
|
|
|
233
260
|
const blockType = block.type;
|
|
234
261
|
if (blockType === 'text' && typeof block.text === 'string') {
|
|
235
262
|
const text = block.text;
|
|
236
|
-
|
|
237
|
-
|
|
263
|
+
if (!msgState.sawText) {
|
|
264
|
+
events.push({ kind: 'message:text', messageId, text, streaming: true });
|
|
265
|
+
msgState.sawText = true;
|
|
266
|
+
}
|
|
238
267
|
if (text.includes('[BRAINSTORM_COMPLETE]')) {
|
|
239
268
|
events.push({ kind: 'session:brainstorm-complete' });
|
|
240
269
|
}
|
|
@@ -42,6 +42,9 @@ export function buildClaudeOptions(input) {
|
|
|
42
42
|
// `~/.claude/settings.json` (where `skipDangerousModePermissionPrompt`
|
|
43
43
|
// lives), breaking bypass for MCP tools.
|
|
44
44
|
settingSources: ['user', 'project', 'local'],
|
|
45
|
+
// Relay the Anthropic API's raw message stream so Kōbō can display true
|
|
46
|
+
// text deltas instead of waiting for complete assistant content blocks.
|
|
47
|
+
includePartialMessages: true,
|
|
45
48
|
};
|
|
46
49
|
switch (input.agentPermissionMode) {
|
|
47
50
|
case 'plan':
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import { getPackageVersion } from '../../../../utils/paths.js';
|
|
2
|
+
import { isWorkspacePermissionAllowed } from '../../../workspace-permission-policy-service.js';
|
|
3
|
+
import { createStreamingBatcher } from '../../streaming-batcher.js';
|
|
2
4
|
import { CODEX_CAPABILITIES } from './capabilities.js';
|
|
3
5
|
import { createAppServerClient } from './client.js';
|
|
4
6
|
import { createMapperState, emitSessionStarted, handleAgentMessageDelta, handleItemCompleted, handleItemStarted, handleRateLimitsUpdated, handleTurnCompleted, QUOTA_PATTERN, tryEmitQuota, } from './event-mapper.js';
|
|
5
7
|
import { buildCodexOptions } from './options-builder.js';
|
|
6
8
|
import { buildResponseForResolve, handleServerRequest } from './server-requests.js';
|
|
7
9
|
import { spawnAppServer } from './spawn.js';
|
|
10
|
+
import { createTurnLiveness } from './turn-liveness.js';
|
|
11
|
+
/** Long enough for normal tool work, short enough to recover a lost turn event. */
|
|
12
|
+
export const CODEX_TURN_IDLE_TIMEOUT_MS = 120_000;
|
|
13
|
+
export const CODEX_GRACEFUL_INTERRUPT_TIMEOUT_MS = 3_000;
|
|
14
|
+
function wait(ms) {
|
|
15
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
|
+
}
|
|
17
|
+
class CodexTurnTimeoutError extends Error {
|
|
18
|
+
constructor() {
|
|
19
|
+
super('Codex stopped reporting activity for this turn');
|
|
20
|
+
this.name = 'CodexTurnTimeoutError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
8
23
|
/**
|
|
9
24
|
* Heuristic for detecting a stale/expired thread id on `thread/resume`.
|
|
10
25
|
* Canonical wording isn't captured yet — when matched, the engine emits
|
|
@@ -34,7 +49,8 @@ export function createCodexEngine() {
|
|
|
34
49
|
let discoveredSessionId = options.resumeFromEngineSessionId;
|
|
35
50
|
let activeTurnId;
|
|
36
51
|
let steerChain = Promise.resolve();
|
|
37
|
-
|
|
52
|
+
let gracefulInterruptPromise;
|
|
53
|
+
const emitDirect = (ev) => {
|
|
38
54
|
try {
|
|
39
55
|
onEvent(ev);
|
|
40
56
|
}
|
|
@@ -42,6 +58,8 @@ export function createCodexEngine() {
|
|
|
42
58
|
console.error('[codex-engine] onEvent handler threw:', err);
|
|
43
59
|
}
|
|
44
60
|
};
|
|
61
|
+
const streamingBatcher = createStreamingBatcher(emitDirect);
|
|
62
|
+
const safeEmit = (ev) => streamingBatcher.push(ev);
|
|
45
63
|
const child = spawnAppServer({ cwd: options.workingDir, signal: abortController.signal });
|
|
46
64
|
if (child.stderr) {
|
|
47
65
|
child.stderr.setEncoding('utf8');
|
|
@@ -61,6 +79,14 @@ export function createCodexEngine() {
|
|
|
61
79
|
resolveTurnDone = resolve;
|
|
62
80
|
rejectTurnDone = reject;
|
|
63
81
|
});
|
|
82
|
+
const turnLiveness = createTurnLiveness({
|
|
83
|
+
timeoutMs: CODEX_TURN_IDLE_TIMEOUT_MS,
|
|
84
|
+
onTimeout() {
|
|
85
|
+
mapperState.sawErrorResult = true;
|
|
86
|
+
safeEmit({ kind: 'error', category: 'other', message: 'Codex stopped reporting activity for this turn' });
|
|
87
|
+
rejectTurnDone(new CodexTurnTimeoutError());
|
|
88
|
+
},
|
|
89
|
+
});
|
|
64
90
|
abortController.signal.addEventListener('abort', () => {
|
|
65
91
|
const err = new Error('AbortError');
|
|
66
92
|
err.name = 'AbortError';
|
|
@@ -71,9 +97,23 @@ export function createCodexEngine() {
|
|
|
71
97
|
stdout: child.stdout,
|
|
72
98
|
clientInfo: { name: 'kobo', version: getPackageVersion() },
|
|
73
99
|
onNotification(method, params) {
|
|
100
|
+
turnLiveness.activity();
|
|
74
101
|
// Ignored notifications — harmless bookkeeping by the server
|
|
75
|
-
if (method === 'mcpServer/startupStatus/updated'
|
|
76
|
-
|
|
102
|
+
if (method === 'mcpServer/startupStatus/updated') {
|
|
103
|
+
const status = (params ?? {});
|
|
104
|
+
const rawStatus = String(status.status ?? status.state ?? 'starting').toLowerCase();
|
|
105
|
+
const normalized = rawStatus.includes('error') || rawStatus.includes('fail')
|
|
106
|
+
? 'error'
|
|
107
|
+
: rawStatus.includes('ready') || rawStatus.includes('running')
|
|
108
|
+
? 'ready'
|
|
109
|
+
: 'starting';
|
|
110
|
+
const serverName = String(status.serverName ?? status.name ?? status.id ?? 'MCP');
|
|
111
|
+
const rawMessage = typeof status.message === 'string' ? status.message : undefined;
|
|
112
|
+
const message = rawMessage?.replace(/(token|api[_-]?key|secret)\s*[:=]\s*\S+/gi, '$1=[redacted]');
|
|
113
|
+
safeEmit({ kind: 'mcp:status', serverName, status: normalized, message });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (method === 'thread/started' ||
|
|
77
117
|
method === 'thread/status/changed' ||
|
|
78
118
|
method === 'remoteControl/status/changed' ||
|
|
79
119
|
method === 'turn/started') {
|
|
@@ -144,8 +184,11 @@ export function createCodexEngine() {
|
|
|
144
184
|
emit: safeEmit,
|
|
145
185
|
register(callId, pending) {
|
|
146
186
|
pendingByCallId.set(callId, pending);
|
|
187
|
+
turnLiveness.pause();
|
|
147
188
|
},
|
|
148
189
|
respondError: (reqId, code, message) => client.peer.respondError(reqId, code, message),
|
|
190
|
+
respond: (reqId, result) => client.peer.respond(reqId, result),
|
|
191
|
+
autoApprove: (toolName, payload) => isWorkspacePermissionAllowed(options.workspaceId, { engine: 'codex', toolName, payload }),
|
|
149
192
|
});
|
|
150
193
|
},
|
|
151
194
|
onError(err) {
|
|
@@ -185,7 +228,9 @@ export function createCodexEngine() {
|
|
|
185
228
|
collaborationMode,
|
|
186
229
|
});
|
|
187
230
|
activeTurnId = initialTurn.turnId;
|
|
231
|
+
turnLiveness.start();
|
|
188
232
|
await turnDonePromise;
|
|
233
|
+
turnLiveness.stop();
|
|
189
234
|
const reason = mapperState.sawErrorResult
|
|
190
235
|
? 'error'
|
|
191
236
|
: mapperState.sawTurnInterrupted
|
|
@@ -198,6 +243,7 @@ export function createCodexEngine() {
|
|
|
198
243
|
});
|
|
199
244
|
}
|
|
200
245
|
catch (err) {
|
|
246
|
+
turnLiveness.stop();
|
|
201
247
|
const error = err;
|
|
202
248
|
const message = error.message ?? String(err);
|
|
203
249
|
const isAbort = userInterrupted || error.name === 'AbortError' || abortController.signal.aborted;
|
|
@@ -205,6 +251,9 @@ export function createCodexEngine() {
|
|
|
205
251
|
if (isAbort) {
|
|
206
252
|
safeEmit({ kind: 'session:ended', reason: 'killed', exitCode: null });
|
|
207
253
|
}
|
|
254
|
+
else if (error.name === 'CodexTurnTimeoutError') {
|
|
255
|
+
safeEmit({ kind: 'session:ended', reason: 'error', exitCode: null });
|
|
256
|
+
}
|
|
208
257
|
else if (QUOTA_PATTERN.test(message)) {
|
|
209
258
|
tryEmitQuota(mapperState, safeEmit, message);
|
|
210
259
|
safeEmit({ kind: 'session:ended', reason: 'error', exitCode: null });
|
|
@@ -219,6 +268,8 @@ export function createCodexEngine() {
|
|
|
219
268
|
}
|
|
220
269
|
}
|
|
221
270
|
finally {
|
|
271
|
+
turnLiveness.stop();
|
|
272
|
+
streamingBatcher.close();
|
|
222
273
|
iteratorRunning = false;
|
|
223
274
|
client.close();
|
|
224
275
|
try {
|
|
@@ -257,13 +308,23 @@ export function createCodexEngine() {
|
|
|
257
308
|
},
|
|
258
309
|
interrupt() {
|
|
259
310
|
userInterrupted = true;
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
311
|
+
if (gracefulInterruptPromise)
|
|
312
|
+
return;
|
|
313
|
+
gracefulInterruptPromise = (async () => {
|
|
314
|
+
if (discoveredSessionId) {
|
|
315
|
+
await Promise.race([
|
|
316
|
+
client.interruptTurn({ threadId: discoveredSessionId }).catch(() => { }),
|
|
317
|
+
wait(CODEX_GRACEFUL_INTERRUPT_TIMEOUT_MS),
|
|
318
|
+
]);
|
|
319
|
+
}
|
|
320
|
+
await Promise.race([turnDonePromise.catch(() => { }), wait(CODEX_GRACEFUL_INTERRUPT_TIMEOUT_MS)]);
|
|
321
|
+
if (iteratorRunning)
|
|
322
|
+
abortController.abort();
|
|
323
|
+
})();
|
|
264
324
|
},
|
|
265
325
|
async stop() {
|
|
266
|
-
|
|
326
|
+
engineProcess.interrupt();
|
|
327
|
+
await gracefulInterruptPromise;
|
|
267
328
|
try {
|
|
268
329
|
await iteratorPromise;
|
|
269
330
|
}
|
|
@@ -282,6 +343,7 @@ export function createCodexEngine() {
|
|
|
282
343
|
if (!pending)
|
|
283
344
|
return false;
|
|
284
345
|
pendingByCallId.delete(callId);
|
|
346
|
+
turnLiveness.resume();
|
|
285
347
|
const result = buildResponseForResolve(pending, response);
|
|
286
348
|
client.peer.respond(pending.requestId, result);
|
|
287
349
|
return true;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export function handleServerRequest(args) {
|
|
2
|
-
const { method, params, requestId, emit, register, respondError } = args;
|
|
2
|
+
const { method, params, requestId, emit, register, respondError, respond, autoApprove } = args;
|
|
3
3
|
const p = (params ?? {});
|
|
4
4
|
const callId = typeof p.callId === 'string' ? p.callId : `srv_${requestId}`;
|
|
5
5
|
if (method === 'mcpServer/elicitation/request') {
|
|
@@ -13,24 +13,34 @@ export function handleServerRequest(args) {
|
|
|
13
13
|
// (`execCommandApproval`, `applyPatchApproval`) are kept for compat with
|
|
14
14
|
// older Codex CLI builds that haven't transitioned to the v2 namespace.
|
|
15
15
|
if (method === 'item/commandExecution/requestApproval' || method === 'execCommandApproval') {
|
|
16
|
+
const payload = { command: p.command, cwd: p.cwd, reason: p.reason };
|
|
17
|
+
if (autoApprove?.('Bash', payload)) {
|
|
18
|
+
respond?.(requestId, { decision: 'accept' });
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
16
21
|
register(callId, { requestId, kind: 'command', payload: p });
|
|
17
22
|
emit({
|
|
18
23
|
kind: 'session:user-input-requested',
|
|
19
24
|
requestKind: 'permission',
|
|
20
25
|
toolCallId: callId,
|
|
21
26
|
toolName: 'Bash',
|
|
22
|
-
payload
|
|
27
|
+
payload,
|
|
23
28
|
});
|
|
24
29
|
return true;
|
|
25
30
|
}
|
|
26
31
|
if (method === 'item/fileChange/requestApproval' || method === 'applyPatchApproval') {
|
|
32
|
+
const payload = { changes: p.changes, reason: p.reason };
|
|
33
|
+
if (autoApprove?.('Edit', payload)) {
|
|
34
|
+
respond?.(requestId, { decision: 'accept' });
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
27
37
|
register(callId, { requestId, kind: 'file_change', payload: p });
|
|
28
38
|
emit({
|
|
29
39
|
kind: 'session:user-input-requested',
|
|
30
40
|
requestKind: 'permission',
|
|
31
41
|
toolCallId: callId,
|
|
32
42
|
toolName: 'Edit',
|
|
33
|
-
payload
|
|
43
|
+
payload,
|
|
34
44
|
});
|
|
35
45
|
return true;
|
|
36
46
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks a single active Codex turn. A pending user decision deliberately
|
|
3
|
+
* pauses the deadline: the agent is waiting on a human, not stalled.
|
|
4
|
+
*/
|
|
5
|
+
export function createTurnLiveness(input) {
|
|
6
|
+
let timer;
|
|
7
|
+
let active = false;
|
|
8
|
+
let paused = false;
|
|
9
|
+
let timedOut = false;
|
|
10
|
+
const arm = () => {
|
|
11
|
+
if (!active || paused || timedOut)
|
|
12
|
+
return;
|
|
13
|
+
if (timer)
|
|
14
|
+
clearTimeout(timer);
|
|
15
|
+
timer = setTimeout(() => {
|
|
16
|
+
timer = undefined;
|
|
17
|
+
if (!active || paused || timedOut)
|
|
18
|
+
return;
|
|
19
|
+
timedOut = true;
|
|
20
|
+
input.onTimeout();
|
|
21
|
+
}, input.timeoutMs);
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
start() {
|
|
25
|
+
active = true;
|
|
26
|
+
paused = false;
|
|
27
|
+
timedOut = false;
|
|
28
|
+
arm();
|
|
29
|
+
},
|
|
30
|
+
activity() {
|
|
31
|
+
arm();
|
|
32
|
+
},
|
|
33
|
+
pause() {
|
|
34
|
+
paused = true;
|
|
35
|
+
if (timer)
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
timer = undefined;
|
|
38
|
+
},
|
|
39
|
+
resume() {
|
|
40
|
+
if (!active || !paused)
|
|
41
|
+
return;
|
|
42
|
+
paused = false;
|
|
43
|
+
arm();
|
|
44
|
+
},
|
|
45
|
+
stop() {
|
|
46
|
+
active = false;
|
|
47
|
+
paused = false;
|
|
48
|
+
if (timer)
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
timer = undefined;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -6,5 +6,9 @@ export function routeEvent(workspaceId, agentSessionId, event) {
|
|
|
6
6
|
emitEphemeral(workspaceId, 'agent:event', event);
|
|
7
7
|
return;
|
|
8
8
|
}
|
|
9
|
+
if (event.kind === 'mcp:status' && event.status !== 'error') {
|
|
10
|
+
emitEphemeral(workspaceId, 'agent:event', event);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
9
13
|
emit(workspaceId, 'agent:event', event, agentSessionId);
|
|
10
14
|
}
|
|
@@ -11,6 +11,7 @@ import { getEffectiveSettings } from '../settings-service.js';
|
|
|
11
11
|
import { refreshNow } from '../usage/poller.js';
|
|
12
12
|
import * as wakeupService from '../wakeup-service.js';
|
|
13
13
|
import { emit, emitEphemeral } from '../websocket-service.js';
|
|
14
|
+
import * as permissionPolicyService from '../workspace-permission-policy-service.js';
|
|
14
15
|
import { getWorkspace as getWs, markWorkspaceUnread, updateWorkspaceStatus, } from '../workspace-service.js';
|
|
15
16
|
import { resolveEngine } from './engines/registry.js';
|
|
16
17
|
import { routeEvent } from './event-router.js';
|
|
@@ -554,6 +555,10 @@ function handleEvent(workspaceId, agentSessionId, ev) {
|
|
|
554
555
|
tasksDoneSnapshot.delete(workspaceId);
|
|
555
556
|
taskStateSnapshot.delete(workspaceId);
|
|
556
557
|
clearPendingForSession(workspaceId, agentSessionId);
|
|
558
|
+
// A completed/failed SDK session cannot resolve canUseTool anymore. Drop
|
|
559
|
+
// its persisted prompts too, otherwise a reconnect resurrects a panel
|
|
560
|
+
// whose backend callback has already been discarded.
|
|
561
|
+
purgeAllPersistedUserInputRequests(workspaceId, agentSessionId);
|
|
557
562
|
// Must run BEFORE autoLoopService.onSessionEnded → spawnNextIteration →
|
|
558
563
|
// startAgent, otherwise startAgent throws "Agent already running" because
|
|
559
564
|
// the just-ended controller is still in the map.
|
|
@@ -698,6 +703,8 @@ function onSessionEnded(workspaceId, agentSessionId, exitCode, reason, resumeFai
|
|
|
698
703
|
* query the controller via `_getControllers()` for tests.
|
|
699
704
|
*/
|
|
700
705
|
export function startAgent(workspaceId, workingDir, prompt, model, resume = false, agentPermissionMode, existingSessionId, reasoningEffort) {
|
|
706
|
+
// "For this turn" never survives a new agent session.
|
|
707
|
+
permissionPolicyService.clearTurnPermissions(workspaceId);
|
|
701
708
|
// Zombie detection: an SDK iterator hung on a never-resolved canUseTool
|
|
702
709
|
// callback can leave its controller in the map after the workspace is
|
|
703
710
|
// logically idle. Evict it instead of refusing the new session.
|
|
@@ -1019,6 +1026,16 @@ export async function answerPendingPermission(workspaceId, decision) {
|
|
|
1019
1026
|
const response = decision.decision === 'allow'
|
|
1020
1027
|
? { kind: 'permission-allow' }
|
|
1021
1028
|
: { kind: 'permission-deny', reason: decision.reason };
|
|
1029
|
+
if (decision.decision === 'allow' && (decision.scope === 'operation' || decision.scope === 'tool')) {
|
|
1030
|
+
permissionPolicyService.createWorkspacePermissionRule(workspaceId, { engine: ws.engine ?? 'claude-code', toolName: head.toolName, payload: head.toolInput }, decision.scope);
|
|
1031
|
+
}
|
|
1032
|
+
if (decision.decision === 'allow' && decision.scope === 'turn') {
|
|
1033
|
+
permissionPolicyService.allowPermissionForTurn(workspaceId, {
|
|
1034
|
+
engine: ws.engine ?? 'claude-code',
|
|
1035
|
+
toolName: head.toolName,
|
|
1036
|
+
payload: head.toolInput,
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1022
1039
|
const resolved = engineProcess.resolvePendingUserInput(decision.toolCallId, response);
|
|
1023
1040
|
if (!resolved) {
|
|
1024
1041
|
throw new Error(`No pending callback for toolCallId '${decision.toolCallId}'`);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Coalesces token deltas before they reach WebSocket persistence. */
|
|
2
|
+
export function createStreamingBatcher(emit, options = {}) {
|
|
3
|
+
const windowMs = options.windowMs ?? 40;
|
|
4
|
+
const pending = new Map();
|
|
5
|
+
const flush = (messageId) => {
|
|
6
|
+
const entry = pending.get(messageId);
|
|
7
|
+
if (!entry)
|
|
8
|
+
return;
|
|
9
|
+
clearTimeout(entry.timer);
|
|
10
|
+
pending.delete(messageId);
|
|
11
|
+
emit({ kind: 'message:text', messageId, text: entry.text, streaming: true });
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
push(event) {
|
|
15
|
+
if (event.kind === 'message:text' && event.streaming) {
|
|
16
|
+
const entry = pending.get(event.messageId);
|
|
17
|
+
if (entry) {
|
|
18
|
+
entry.text += event.text;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const timer = setTimeout(() => flush(event.messageId), windowMs);
|
|
22
|
+
pending.set(event.messageId, { text: event.text, timer });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (event.kind === 'message:end')
|
|
26
|
+
flush(event.messageId);
|
|
27
|
+
emit(event);
|
|
28
|
+
},
|
|
29
|
+
close() {
|
|
30
|
+
for (const messageId of [...pending.keys()])
|
|
31
|
+
flush(messageId);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -92,7 +92,7 @@ function availabilityFromError(err) {
|
|
|
92
92
|
}
|
|
93
93
|
export const githubProvider = {
|
|
94
94
|
id: 'github',
|
|
95
|
-
capabilities: { canCreatePr: true, canChangePrBase: true, requestTermShort: 'PR' },
|
|
95
|
+
capabilities: { canCreatePr: true, canChangePrBase: true, canMergeRequest: true, requestTermShort: 'PR' },
|
|
96
96
|
async isAvailable(repoPath) {
|
|
97
97
|
try {
|
|
98
98
|
await execFileAsync('gh', ['auth', 'status'], { cwd: repoPath, encoding: 'utf-8' });
|
|
@@ -127,4 +127,7 @@ export const githubProvider = {
|
|
|
127
127
|
async changePrBase(repoPath, base) {
|
|
128
128
|
await execFileAsync('gh', ['pr', 'edit', '--base', base], { cwd: repoPath, encoding: 'utf-8' });
|
|
129
129
|
},
|
|
130
|
+
async mergeRequest(repoPath, number) {
|
|
131
|
+
await execFileAsync('gh', ['pr', 'merge', String(number), '--merge'], { cwd: repoPath, encoding: 'utf-8' });
|
|
132
|
+
},
|
|
130
133
|
};
|
|
@@ -129,7 +129,7 @@ function availabilityFromError(err) {
|
|
|
129
129
|
}
|
|
130
130
|
export const gitlabProvider = {
|
|
131
131
|
id: 'gitlab',
|
|
132
|
-
capabilities: { canCreatePr: true, canChangePrBase: true, requestTermShort: 'MR' },
|
|
132
|
+
capabilities: { canCreatePr: true, canChangePrBase: true, canMergeRequest: true, requestTermShort: 'MR' },
|
|
133
133
|
async isAvailable(repoPath) {
|
|
134
134
|
try {
|
|
135
135
|
await execFileAsync('glab', ['auth', 'status'], { cwd: repoPath, encoding: 'utf-8' });
|
|
@@ -179,4 +179,7 @@ export const gitlabProvider = {
|
|
|
179
179
|
async changePrBase(repoPath, base) {
|
|
180
180
|
await execFileAsync('glab', ['mr', 'update', '--target-branch', base], { cwd: repoPath, encoding: 'utf-8' });
|
|
181
181
|
},
|
|
182
|
+
async mergeRequest(repoPath, number) {
|
|
183
|
+
await execFileAsync('glab', ['mr', 'merge', String(number), '--yes'], { cwd: repoPath, encoding: 'utf-8' });
|
|
184
|
+
},
|
|
182
185
|
};
|
|
@@ -7,7 +7,7 @@ import { ForgeUnavailableError } from './types.js';
|
|
|
7
7
|
*/
|
|
8
8
|
export const noneProvider = {
|
|
9
9
|
id: 'none',
|
|
10
|
-
capabilities: { canCreatePr: false, canChangePrBase: false, requestTermShort: 'PR' },
|
|
10
|
+
capabilities: { canCreatePr: false, canChangePrBase: false, canMergeRequest: false, requestTermShort: 'PR' },
|
|
11
11
|
async isAvailable() {
|
|
12
12
|
return { available: false };
|
|
13
13
|
},
|
|
@@ -20,4 +20,7 @@ export const noneProvider = {
|
|
|
20
20
|
async changePrBase() {
|
|
21
21
|
throw new ForgeUnavailableError('This project has no supported forge configured');
|
|
22
22
|
},
|
|
23
|
+
async mergeRequest() {
|
|
24
|
+
throw new ForgeUnavailableError('This project has no supported forge configured');
|
|
25
|
+
},
|
|
23
26
|
};
|
|
@@ -269,6 +269,25 @@ export async function extractNotionPage(notionUrl) {
|
|
|
269
269
|
mcpProcess.kill();
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
|
+
/** Lightweight MCP/auth smoke test; does not read a user page or mutate Notion. */
|
|
273
|
+
export async function testNotionConnection() {
|
|
274
|
+
const startedAt = Date.now();
|
|
275
|
+
const global = getGlobalSettings();
|
|
276
|
+
const mcpProcess = spawnNotionMcp(global.notionMcpKey);
|
|
277
|
+
try {
|
|
278
|
+
await initializeMcp(mcpProcess);
|
|
279
|
+
await callMcpTool(mcpProcess, 'API-get-users', { page_size: 1 });
|
|
280
|
+
return {
|
|
281
|
+
ok: true,
|
|
282
|
+
durationMs: Date.now() - startedAt,
|
|
283
|
+
detail: 'MCP connection and Notion authentication succeeded',
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
mcpProcess.stdin?.end();
|
|
288
|
+
mcpProcess.kill();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
272
291
|
export async function listNotionUsers() {
|
|
273
292
|
const global = getGlobalSettings();
|
|
274
293
|
const mcpProcess = spawnNotionMcp(global.notionMcpKey);
|
|
@@ -61,7 +61,7 @@ export function searchEvents(query, options = {}) {
|
|
|
61
61
|
// realistic datasets, without scanning the entire table.
|
|
62
62
|
const dbLimit = Math.max(limit * 3, 100);
|
|
63
63
|
const rows = db
|
|
64
|
-
.prepare(`SELECT e.workspace_id, e.type, e.created_at, e.payload,
|
|
64
|
+
.prepare(`SELECT e.id, e.workspace_id, e.session_id, e.type, e.created_at, e.payload,
|
|
65
65
|
w.name AS workspace_name, w.archived_at
|
|
66
66
|
FROM ws_events e
|
|
67
67
|
JOIN workspaces w ON e.workspace_id = w.id
|
|
@@ -88,6 +88,8 @@ export function searchEvents(query, options = {}) {
|
|
|
88
88
|
if (idx < 0)
|
|
89
89
|
continue;
|
|
90
90
|
results.push({
|
|
91
|
+
eventId: row.id,
|
|
92
|
+
sessionId: row.session_id,
|
|
91
93
|
workspaceId: row.workspace_id,
|
|
92
94
|
workspaceName: row.workspace_name,
|
|
93
95
|
archived: row.archived_at !== null,
|
|
@@ -13,6 +13,26 @@ export function parseSentryUrl(url) {
|
|
|
13
13
|
}
|
|
14
14
|
return match[1];
|
|
15
15
|
}
|
|
16
|
+
/** Lightweight MCP/auth smoke test; does not access or mutate an issue. */
|
|
17
|
+
export async function testSentryConnection() {
|
|
18
|
+
const startedAt = Date.now();
|
|
19
|
+
const global = getGlobalSettings();
|
|
20
|
+
const config = readSentryMcpConfig(global.sentryMcpKey);
|
|
21
|
+
const mcpProcess = spawnMcpProcess(config.command, config.args, config.env);
|
|
22
|
+
try {
|
|
23
|
+
await initializeMcp(mcpProcess);
|
|
24
|
+
await callMcpTool(mcpProcess, 'whoami', {});
|
|
25
|
+
return {
|
|
26
|
+
ok: true,
|
|
27
|
+
durationMs: Date.now() - startedAt,
|
|
28
|
+
detail: 'MCP connection and Sentry authentication succeeded',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
mcpProcess.stdin?.end();
|
|
33
|
+
mcpProcess.kill();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
16
36
|
const SENTRY_CONFIG_ERROR = "Sentry MCP server not configured in ~/.claude.json — add an enabled 'sentry' entry under mcpServers";
|
|
17
37
|
/**
|
|
18
38
|
* Return the first enabled (`disabled !== true`) MCP server entry from
|
|
@@ -750,6 +750,24 @@ const settingsMigrations = [
|
|
|
750
750
|
}
|
|
751
751
|
},
|
|
752
752
|
},
|
|
753
|
+
{
|
|
754
|
+
version: 46,
|
|
755
|
+
name: 'add-integration-enabled-switches',
|
|
756
|
+
migrate: ({ global }) => {
|
|
757
|
+
if (typeof global.notionEnabled !== 'boolean')
|
|
758
|
+
global.notionEnabled = true;
|
|
759
|
+
if (typeof global.sentryEnabled !== 'boolean')
|
|
760
|
+
global.sentryEnabled = true;
|
|
761
|
+
},
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
version: 47,
|
|
765
|
+
name: 'add-thinking-block-visibility',
|
|
766
|
+
migrate: ({ global }) => {
|
|
767
|
+
if (typeof global.showThinkingBlocks !== 'boolean')
|
|
768
|
+
global.showThinkingBlocks = true;
|
|
769
|
+
},
|
|
770
|
+
},
|
|
753
771
|
];
|
|
754
772
|
/** Current settings schema version — always equals the highest migration version. */
|
|
755
773
|
export const SETTINGS_SCHEMA_VERSION = settingsMigrations.length > 0 ? settingsMigrations[settingsMigrations.length - 1].version : 0;
|
|
@@ -837,6 +855,9 @@ function defaultSettings() {
|
|
|
837
855
|
defaultPermissionModeByEngine: { 'claude-code': 'plan', codex: 'plan' },
|
|
838
856
|
notionMcpKey: '',
|
|
839
857
|
sentryMcpKey: '',
|
|
858
|
+
notionEnabled: true,
|
|
859
|
+
sentryEnabled: true,
|
|
860
|
+
showThinkingBlocks: true,
|
|
840
861
|
tags: [...DEFAULT_WORKSPACE_TAGS],
|
|
841
862
|
branchPrefixes: [...DEFAULT_BRANCH_PREFIXES],
|
|
842
863
|
worktreesPath: WORKTREES_PATH,
|
|
@@ -1219,6 +1240,9 @@ export function updateGlobalSettings(data) {
|
|
|
1219
1240
|
'defaultPermissionModeByEngine',
|
|
1220
1241
|
'notionMcpKey',
|
|
1221
1242
|
'sentryMcpKey',
|
|
1243
|
+
'notionEnabled',
|
|
1244
|
+
'sentryEnabled',
|
|
1245
|
+
'showThinkingBlocks',
|
|
1222
1246
|
'tags',
|
|
1223
1247
|
'branchPrefixes',
|
|
1224
1248
|
'worktreesPath',
|