@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.
Files changed (232) hide show
  1. package/AGENTS.md +5 -4
  2. package/CHANGELOG.md +31 -0
  3. package/README.md +16 -10
  4. package/dist/mcp-server/kobo-tasks-server.js +1 -1
  5. package/dist/server/db/migrations.js +21 -0
  6. package/dist/server/db/schema.js +14 -0
  7. package/dist/server/routes/dev-server.js +14 -0
  8. package/dist/server/routes/health.js +2 -2
  9. package/dist/server/routes/notion.js +18 -1
  10. package/dist/server/routes/sentry.js +15 -1
  11. package/dist/server/routes/workspaces.js +221 -6
  12. package/dist/server/services/agent/engines/claude-code/engine.js +17 -1
  13. package/dist/server/services/agent/engines/claude-code/event-mapper.js +31 -2
  14. package/dist/server/services/agent/engines/claude-code/options-builder.js +3 -0
  15. package/dist/server/services/agent/engines/codex/engine.js +70 -8
  16. package/dist/server/services/agent/engines/codex/server-requests.js +13 -3
  17. package/dist/server/services/agent/engines/codex/turn-liveness.js +53 -0
  18. package/dist/server/services/agent/event-router.js +4 -0
  19. package/dist/server/services/agent/orchestrator.js +17 -0
  20. package/dist/server/services/agent/streaming-batcher.js +34 -0
  21. package/dist/server/services/forge/github/provider.js +4 -1
  22. package/dist/server/services/forge/gitlab/provider.js +4 -1
  23. package/dist/server/services/forge/none.js +4 -1
  24. package/dist/server/services/notion-service.js +19 -0
  25. package/dist/server/services/search-service.js +3 -1
  26. package/dist/server/services/sentry-service.js +20 -0
  27. package/dist/server/services/settings-service.js +24 -0
  28. package/dist/server/services/workspace-permission-policy-service.js +86 -0
  29. package/dist/server/utils/paths.js +3 -3
  30. package/package.json +3 -3
  31. package/src/client/dist/pwa/assets/ActivityFeed-ByJjMo0k.js +8 -0
  32. package/src/client/dist/pwa/assets/ActivityFeed-D9EfbRqv.css +1 -0
  33. package/src/client/dist/{spa/assets/ChangelogPage-OyS0LqjP.js → pwa/assets/ChangelogPage-CppcIG5K.js} +1 -1
  34. package/src/client/dist/{spa/assets/ClosePopup-B1wf7Gh3.js → pwa/assets/ClosePopup-B0-qi7Jo.js} +1 -1
  35. package/src/client/dist/pwa/assets/CreatePage-D0xvdJAQ.js +2 -0
  36. package/src/client/dist/pwa/assets/CreatePage-qMgdzdb-.css +1 -0
  37. package/src/client/dist/pwa/assets/DiffViewer-COD5Mdp_.js +8 -0
  38. package/src/client/dist/{spa/assets/DiffViewer-BuYD1EDP.css → pwa/assets/DiffViewer-CRrDdYBa.css} +1 -1
  39. package/src/client/dist/{spa/assets/HealthPage-QMan-G3D.js → pwa/assets/HealthPage-Bnad2BVm.js} +1 -1
  40. package/src/client/dist/pwa/assets/MainLayout-Bwulksar.css +1 -0
  41. package/src/client/dist/pwa/assets/MainLayout-nn4YIcy8.js +37 -0
  42. package/src/client/dist/{spa/assets/QBtnToggle-BKi1Jbys.js → pwa/assets/QBtnToggle-CZXgD3Hf.js} +1 -1
  43. package/src/client/dist/{spa/assets/QChip-1uTELmCE.js → pwa/assets/QChip-BJH-iJCV.js} +1 -1
  44. package/src/client/dist/{spa/assets/QSelect-CKc3IVuT.js → pwa/assets/QSelect-tsFTetLi.js} +1 -1
  45. package/src/client/dist/{spa/assets/QTooltip-86biX27D.js → pwa/assets/QTooltip-CA_Iyybc.js} +1 -1
  46. package/src/client/dist/pwa/assets/SearchPage-B08_UXDs.css +1 -0
  47. package/src/client/dist/pwa/assets/SearchPage-BwD6K8qD.js +1 -0
  48. package/src/client/dist/pwa/assets/SettingsPage-CF-xI5PO.js +16 -0
  49. package/src/client/dist/{spa/assets/SettingsPage-DGAGBtQ2.css → pwa/assets/SettingsPage-DJEH2ahl.css} +1 -1
  50. package/src/client/dist/pwa/assets/ThinkingItem-Ckk4K1O-.css +1 -0
  51. package/src/client/dist/pwa/assets/ThinkingItem-DBWuhjgW.js +1 -0
  52. package/src/client/dist/{spa/assets/TouchPan-EYoNwghH.js → pwa/assets/TouchPan-txugr_lm.js} +1 -1
  53. package/src/client/dist/pwa/assets/WorkspacePage-Cv04uh_0.css +1 -0
  54. package/src/client/dist/pwa/assets/WorkspacePage-DgucXHjd.js +4 -0
  55. package/src/client/dist/{spa/assets/build-path-tree-BAxSTjwb.js → pwa/assets/build-path-tree-BUkEDxlT.js} +1 -1
  56. package/src/client/dist/{spa/assets/cssMode-CG3m7iQl.js → pwa/assets/cssMode-BDLIXvw9.js} +1 -1
  57. package/src/client/dist/{spa/assets/documents-Dz8sHeyR.js → pwa/assets/documents-ClS4k_bT.js} +1 -1
  58. package/src/client/dist/{spa/assets/editor.api2-DqJsxy06.js → pwa/assets/editor.api2-PhaZVUXg.js} +1 -1
  59. package/src/client/dist/{spa/assets/editor.main-r2oDGbHA.js → pwa/assets/editor.main-2nNvSQc1.js} +2 -2
  60. package/src/client/dist/{spa/assets/expand-template-DP5gf1ro.js → pwa/assets/expand-template-HgUyAUBK.js} +1 -1
  61. package/src/client/dist/{spa/assets/formatters-EQwGVQQS.js → pwa/assets/formatters-Chk4XGyr.js} +1 -1
  62. package/src/client/dist/{spa/assets/freemarker2-C6Dcsusp.js → pwa/assets/freemarker2-Dy02xxKp.js} +1 -1
  63. package/src/client/dist/{spa/assets/handlebars-BYzVrRyn.js → pwa/assets/handlebars-Do59Qec5.js} +1 -1
  64. package/src/client/dist/{spa/assets/html-Yuf2QU9u.js → pwa/assets/html-MJ9-LLjS.js} +1 -1
  65. package/src/client/dist/{spa/assets/htmlMode-BJJsbkSn.js → pwa/assets/htmlMode-AsZ-SMzZ.js} +1 -1
  66. package/src/client/dist/pwa/assets/i18n-Djv0PzK4.js +1 -0
  67. package/src/client/dist/pwa/assets/index-BpTtR9lB.js +84 -0
  68. package/src/client/dist/{spa/assets/javascript-Ch3ueiOd.js → pwa/assets/javascript-DoZFpstd.js} +1 -1
  69. package/src/client/dist/{spa/assets/jsonMode-xEENY1ms.js → pwa/assets/jsonMode-CMeFz2Y2.js} +1 -1
  70. package/src/client/dist/{spa/assets/kobo-commands-CbIH5Wgu.js → pwa/assets/kobo-commands-DmJq1I6k.js} +1 -1
  71. package/src/client/dist/{spa/assets/layout-EwsT5w-M.js → pwa/assets/layout-BSJkgTMQ.js} +1 -1
  72. package/src/client/dist/{spa/assets/liquid-Bh89yiMp.js → pwa/assets/liquid-Bc70ZdlV.js} +1 -1
  73. package/src/client/dist/{spa/assets/lspLanguageFeatures-CNi35cTh.js → pwa/assets/lspLanguageFeatures-Co4WnPYT.js} +1 -1
  74. package/src/client/dist/{spa/assets/mdx-Pk_6m6B2.js → pwa/assets/mdx-BsXN2I7z.js} +1 -1
  75. package/src/client/dist/{spa/assets/monaco.contribution-DyjS5cak.js → pwa/assets/monaco.contribution-BpmQVvpQ.js} +2 -2
  76. package/src/client/dist/{spa/assets/network-auth-B4eYLa1I.js → pwa/assets/network-auth-B0RCBQ5K.js} +1 -1
  77. package/src/client/dist/{spa/assets/QScrollArea-ehpkh2GE.js → pwa/assets/pending-diff-open-BiozufWe.js} +1 -1
  78. package/src/client/dist/{spa/assets/QExpansionItem-BJZyoT79.js → pwa/assets/permissionModes-kRWgzgvi.js} +1 -1
  79. package/src/client/dist/{spa/assets/project-color-8ANVbJax.js → pwa/assets/project-color-D35ZMh9_.js} +1 -1
  80. package/src/client/dist/{spa/assets/python-7GmD1T0-.js → pwa/assets/python-CV5Zt8gJ.js} +1 -1
  81. package/src/client/dist/{spa/assets/razor-CTW-7qAs.js → pwa/assets/razor-4Ev2x3ig.js} +1 -1
  82. package/src/client/dist/{spa/assets/render-chat-markdown-iOBona09.js → pwa/assets/render-chat-markdown-Cjt11WnX.js} +1 -1
  83. package/src/client/dist/{spa/assets/tsMode-BMCTiDkj.js → pwa/assets/tsMode-BDysTsn1.js} +1 -1
  84. package/src/client/dist/{spa/assets/typescript-CBNlJ2bB.js → pwa/assets/typescript-DM1CefPN.js} +1 -1
  85. package/src/client/dist/{spa/assets/use-onboarding-C7pg8RV8.js → pwa/assets/use-onboarding-D2BGKiP_.js} +1 -1
  86. package/src/client/dist/{spa/assets/use-quasar-by40ecAs.js → pwa/assets/use-quasar-Cps11O9Y.js} +1 -1
  87. package/src/client/dist/{spa/assets/workers-CrZaPx7e.js → pwa/assets/workers-BuJZJ6j6.js} +1 -1
  88. package/src/client/dist/{spa/assets/xml-CkKKuUBJ.js → pwa/assets/xml-DzARVQ9o.js} +1 -1
  89. package/src/client/dist/{spa/assets/yaml-B6VtPgSa.js → pwa/assets/yaml-P0ZYSZvj.js} +1 -1
  90. package/src/client/dist/{spa → pwa}/index.html +2 -2
  91. package/src/client/dist/pwa/manifest.json +1 -0
  92. package/src/client/dist/pwa/sw.js +1 -0
  93. package/src/client/dist/pwa/workbox-778a14ff.js +1 -0
  94. package/src/mcp-server/kobo-tasks-server.ts +1 -1
  95. package/src/client/dist/spa/assets/ActivityFeed-Bn4IG1gM.js +0 -8
  96. package/src/client/dist/spa/assets/ActivityFeed-HbMZOClq.css +0 -1
  97. package/src/client/dist/spa/assets/CreatePage-C1yAhjsO.css +0 -1
  98. package/src/client/dist/spa/assets/CreatePage-CkOwo6Jn.js +0 -2
  99. package/src/client/dist/spa/assets/DiffViewer-DUy5tfI3.js +0 -8
  100. package/src/client/dist/spa/assets/MainLayout-7nwhu8UE.css +0 -1
  101. package/src/client/dist/spa/assets/MainLayout-CkVcmDeg.js +0 -37
  102. package/src/client/dist/spa/assets/SearchPage-V8OnALqt.js +0 -1
  103. package/src/client/dist/spa/assets/SearchPage-cVwt0DaQ.css +0 -1
  104. package/src/client/dist/spa/assets/SettingsPage-CazPsJnl.js +0 -16
  105. package/src/client/dist/spa/assets/WorkspacePage-BOApCawq.css +0 -1
  106. package/src/client/dist/spa/assets/WorkspacePage-q6R6SPuh.js +0 -4
  107. package/src/client/dist/spa/assets/i18n-CPaxvFKM.js +0 -1
  108. package/src/client/dist/spa/assets/index-CC1iOXc-.js +0 -84
  109. package/src/client/dist/spa/assets/permissionModes-6jhegAm-.js +0 -1
  110. /package/src/client/dist/{spa → pwa}/assets/ChangelogPage-CQ33An2f.css +0 -0
  111. /package/src/client/dist/{spa → pwa}/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuYjalmUiAw-BCOjigVR.woff +0 -0
  112. /package/src/client/dist/{spa → pwa}/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuZtalmUiAw-Ocp2tNig.woff +0 -0
  113. /package/src/client/dist/{spa → pwa}/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuaabVmUiAw-6jEn9xEl.woff +0 -0
  114. /package/src/client/dist/{spa → pwa}/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWub2bVmUiAw-CCs8I9zJ.woff +0 -0
  115. /package/src/client/dist/{spa → pwa}/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbFmUiAw-BLaboPi0.woff +0 -0
  116. /package/src/client/dist/{spa → pwa}/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAw-DHuK6FBl.woff +0 -0
  117. /package/src/client/dist/{spa → pwa}/assets/QBadge-DV5kn4mL.js +0 -0
  118. /package/src/client/dist/{spa → pwa}/assets/QBanner-CmfaeizG.js +0 -0
  119. /package/src/client/dist/{spa → pwa}/assets/QBtnGroup-C442-yBb.js +0 -0
  120. /package/src/client/dist/{spa → pwa}/assets/QIcon-C6E-oEjI.js +0 -0
  121. /package/src/client/dist/{spa → pwa}/assets/QInput-BF8x4RTv.js +0 -0
  122. /package/src/client/dist/{spa → pwa}/assets/QList-D5EKdFPn.js +0 -0
  123. /package/src/client/dist/{spa → pwa}/assets/QPage-D3rjwA1l.js +0 -0
  124. /package/src/client/dist/{spa → pwa}/assets/QSeparator-DWTCQxsa.js +0 -0
  125. /package/src/client/dist/{spa → pwa}/assets/QSpace-ezNrZkuj.js +0 -0
  126. /package/src/client/dist/{spa → pwa}/assets/QSpinnerDots-DIV68-cV.js +0 -0
  127. /package/src/client/dist/{spa → pwa}/assets/_plugin-vue_export-helper-BDNMzG2s.js +0 -0
  128. /package/src/client/dist/{spa → pwa}/assets/abap-08VXUWAP.js +0 -0
  129. /package/src/client/dist/{spa → pwa}/assets/apex-BWPQTe0t.js +0 -0
  130. /package/src/client/dist/{spa → pwa}/assets/azcli-Bc_sGQ0U.js +0 -0
  131. /package/src/client/dist/{spa → pwa}/assets/bat-i0X4ZdIN.js +0 -0
  132. /package/src/client/dist/{spa → pwa}/assets/bicep-B5-_aFwp.js +0 -0
  133. /package/src/client/dist/{spa → pwa}/assets/cameligo-DMUM7wLl.js +0 -0
  134. /package/src/client/dist/{spa → pwa}/assets/chunk-QTnfLwEv.js +0 -0
  135. /package/src/client/dist/{spa → pwa}/assets/clojure-Cm7r79vr.js +0 -0
  136. /package/src/client/dist/{spa → pwa}/assets/codicon-ngg6Pgfi.ttf +0 -0
  137. /package/src/client/dist/{spa → pwa}/assets/coffee-Ba7i2nA0.js +0 -0
  138. /package/src/client/dist/{spa → pwa}/assets/cpp-C7h46wYY.js +0 -0
  139. /package/src/client/dist/{spa → pwa}/assets/csharp-BKxtCVv1.js +0 -0
  140. /package/src/client/dist/{spa → pwa}/assets/csp-bTuwJoIa.js +0 -0
  141. /package/src/client/dist/{spa → pwa}/assets/css-DIMkf-bt.js +0 -0
  142. /package/src/client/dist/{spa → pwa}/assets/css.worker-CBlhdqTa.js +0 -0
  143. /package/src/client/dist/{spa → pwa}/assets/cypher-CVaqCwHa.js +0 -0
  144. /package/src/client/dist/{spa → pwa}/assets/dart-onAF5SnQ.js +0 -0
  145. /package/src/client/dist/{spa → pwa}/assets/dockerfile-DZFCIeNp.js +0 -0
  146. /package/src/client/dist/{spa → pwa}/assets/ecl-D05T4iGw.js +0 -0
  147. /package/src/client/dist/{spa → pwa}/assets/editor-BFebQ78e.css +0 -0
  148. /package/src/client/dist/{spa → pwa}/assets/editor.worker-Cu3tR8iJ.js +0 -0
  149. /package/src/client/dist/{spa → pwa}/assets/elixir-6RTg0lbw.js +0 -0
  150. /package/src/client/dist/{spa → pwa}/assets/expand-template-D2yUa54D.css +0 -0
  151. /package/src/client/dist/{spa → pwa}/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
  152. /package/src/client/dist/{spa → pwa}/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ-D-x-0Q06.woff2 +0 -0
  153. /package/src/client/dist/{spa → pwa}/assets/flow9-C5_-GSwl.js +0 -0
  154. /package/src/client/dist/{spa → pwa}/assets/fsharp-C8Ef5oNN.js +0 -0
  155. /package/src/client/dist/{spa → pwa}/assets/go-C-y9NEjX.js +0 -0
  156. /package/src/client/dist/{spa → pwa}/assets/graphql-fmXr3nnJ.js +0 -0
  157. /package/src/client/dist/{spa → pwa}/assets/hcl-CpzslTdj.js +0 -0
  158. /package/src/client/dist/{spa → pwa}/assets/html.worker-BKBrNna1.js +0 -0
  159. /package/src/client/dist/{spa → pwa}/assets/index-CwYR3iHt.css +0 -0
  160. /package/src/client/dist/{spa → pwa}/assets/ini-sBoK_t0W.js +0 -0
  161. /package/src/client/dist/{spa → pwa}/assets/java-BEtHBSE6.js +0 -0
  162. /package/src/client/dist/{spa → pwa}/assets/json.worker-BaLYt9Ss.js +0 -0
  163. /package/src/client/dist/{spa → pwa}/assets/julia-Bri6UV-V.js +0 -0
  164. /package/src/client/dist/{spa → pwa}/assets/kotlin-BOotOW0E.js +0 -0
  165. /package/src/client/dist/{spa → pwa}/assets/less-B9JPFI3C.js +0 -0
  166. /package/src/client/dist/{spa → pwa}/assets/lexon-CfSJPG6W.js +0 -0
  167. /package/src/client/dist/{spa → pwa}/assets/lua-CsQS60Ue.js +0 -0
  168. /package/src/client/dist/{spa → pwa}/assets/m3-D-oSqn_W.js +0 -0
  169. /package/src/client/dist/{spa → pwa}/assets/markdown-Cimd5fb3.js +0 -0
  170. /package/src/client/dist/{spa → pwa}/assets/mips-CIPQ_RoX.js +0 -0
  171. /package/src/client/dist/{spa → pwa}/assets/msdax-DauUninz.js +0 -0
  172. /package/src/client/dist/{spa → pwa}/assets/mysql-SOo6toE5.js +0 -0
  173. /package/src/client/dist/{spa → pwa}/assets/objective-c-FvmIjYaQ.js +0 -0
  174. /package/src/client/dist/{spa → pwa}/assets/pascal-DrH0SRf2.js +0 -0
  175. /package/src/client/dist/{spa → pwa}/assets/pascaligo-D-ptJ9y-.js +0 -0
  176. /package/src/client/dist/{spa → pwa}/assets/perl-oz_6vUea.js +0 -0
  177. /package/src/client/dist/{spa → pwa}/assets/pgsql-DTj74zXo.js +0 -0
  178. /package/src/client/dist/{spa → pwa}/assets/php-nr791fC2.js +0 -0
  179. /package/src/client/dist/{spa → pwa}/assets/pla-CopQ2nXW.js +0 -0
  180. /package/src/client/dist/{spa → pwa}/assets/postiats-43DmfD33.js +0 -0
  181. /package/src/client/dist/{spa → pwa}/assets/powerquery-D3hlyOfw.js +0 -0
  182. /package/src/client/dist/{spa → pwa}/assets/powershell-DmHpPYUd.js +0 -0
  183. /package/src/client/dist/{spa → pwa}/assets/protobuf-C531GsRP.js +0 -0
  184. /package/src/client/dist/{spa → pwa}/assets/pug-Z5eAx3Zn.js +0 -0
  185. /package/src/client/dist/{spa → pwa}/assets/qsharp-DkqhCAOL.js +0 -0
  186. /package/src/client/dist/{spa → pwa}/assets/r-BwWrilGY.js +0 -0
  187. /package/src/client/dist/{spa → pwa}/assets/redis-ClamHrr6.js +0 -0
  188. /package/src/client/dist/{spa → pwa}/assets/redshift-DT7zqm-g.js +0 -0
  189. /package/src/client/dist/{spa → pwa}/assets/restructuredtext-BYgofb2h.js +0 -0
  190. /package/src/client/dist/{spa → pwa}/assets/ruby-DezsRK8O.js +0 -0
  191. /package/src/client/dist/{spa → pwa}/assets/rust-DdL9SqIa.js +0 -0
  192. /package/src/client/dist/{spa → pwa}/assets/sb-CcwsVR0C.js +0 -0
  193. /package/src/client/dist/{spa → pwa}/assets/scala-DHpiXF5c.js +0 -0
  194. /package/src/client/dist/{spa → pwa}/assets/scheme-BeGwcela.js +0 -0
  195. /package/src/client/dist/{spa → pwa}/assets/scss-gp-XZpBa.js +0 -0
  196. /package/src/client/dist/{spa → pwa}/assets/shell-CC2rA5mh.js +0 -0
  197. /package/src/client/dist/{spa → pwa}/assets/solidity-BEEn4gHE.js +0 -0
  198. /package/src/client/dist/{spa → pwa}/assets/sophia-CRfGWb83.js +0 -0
  199. /package/src/client/dist/{spa → pwa}/assets/sparql-D_Lu-MrJ.js +0 -0
  200. /package/src/client/dist/{spa → pwa}/assets/sql-NEE52Syq.js +0 -0
  201. /package/src/client/dist/{spa → pwa}/assets/st-DbInun42.js +0 -0
  202. /package/src/client/dist/{spa → pwa}/assets/swift-Bxkupp3x.js +0 -0
  203. /package/src/client/dist/{spa → pwa}/assets/systemverilog-Bz4Y3fRF.js +0 -0
  204. /package/src/client/dist/{spa → pwa}/assets/tcl-DISqw1ZD.js +0 -0
  205. /package/src/client/dist/{spa → pwa}/assets/ts.worker-PmaSgaZk.js +0 -0
  206. /package/src/client/dist/{spa → pwa}/assets/twig-De2hgUGE.js +0 -0
  207. /package/src/client/dist/{spa → pwa}/assets/typespec-B8J7ngcE.js +0 -0
  208. /package/src/client/dist/{spa → pwa}/assets/use-checkbox-E9SV7iH5.js +0 -0
  209. /package/src/client/dist/{spa → pwa}/assets/use-onboarding-B3l7mx48.css +0 -0
  210. /package/src/client/dist/{spa → pwa}/assets/vb-DV3o63ZY.js +0 -0
  211. /package/src/client/dist/{spa → pwa}/assets/vue.runtime.esm-bundler-JZnIeD9D.js +0 -0
  212. /package/src/client/dist/{spa → pwa}/assets/wgsl-DpFanUEy.js +0 -0
  213. /package/src/client/dist/{spa → pwa}/favicon.svg +0 -0
  214. /package/src/client/dist/{spa → pwa}/sounds/7eme-compagnie-03.mp3 +0 -0
  215. /package/src/client/dist/{spa → pwa}/sounds/aller-ftg.mp3 +0 -0
  216. /package/src/client/dist/{spa → pwa}/sounds/arrete-de-mentir.mp3 +0 -0
  217. /package/src/client/dist/{spa → pwa}/sounds/arretez-les-messages.mp3 +0 -0
  218. /package/src/client/dist/{spa → pwa}/sounds/bah-alors-on-est-nul.mp3 +0 -0
  219. /package/src/client/dist/{spa → pwa}/sounds/ca_va_peter.mp3 +0 -0
  220. /package/src/client/dist/{spa → pwa}/sounds/dry-fart.mp3 +0 -0
  221. /package/src/client/dist/{spa → pwa}/sounds/faaah.mp3 +0 -0
  222. /package/src/client/dist/{spa → pwa}/sounds/for-shure.mp3 +0 -0
  223. /package/src/client/dist/{spa → pwa}/sounds/gta-v-death.mp3 +0 -0
  224. /package/src/client/dist/{spa → pwa}/sounds/hey.mp3 +0 -0
  225. /package/src/client/dist/{spa → pwa}/sounds/nan-tu-degages.mp3 +0 -0
  226. /package/src/client/dist/{spa → pwa}/sounds/nan-wallah-pardon.mp3 +0 -0
  227. /package/src/client/dist/{spa → pwa}/sounds/ouais-cest-greg.mp3 +0 -0
  228. /package/src/client/dist/{spa → pwa}/sounds/pas-ca-zinedine.mp3 +0 -0
  229. /package/src/client/dist/{spa → pwa}/sounds/ta-gueule.mp3 +0 -0
  230. /package/src/client/dist/{spa → pwa}/sounds/travail_termine.mp3 +0 -0
  231. /package/src/client/dist/{spa → pwa}/sounds/tu-vas-la-fermer.mp3 +0 -0
  232. /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 safeEmit = (ev) => {
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
- events.push({ kind: 'message:text', messageId, text, streaming: true });
237
- msgState.sawText = true;
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
- const safeEmit = (ev) => {
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
- method === 'thread/started' ||
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
- abortController.abort();
261
- if (discoveredSessionId) {
262
- client.interruptTurn({ threadId: discoveredSessionId }).catch(() => { });
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
- abortController.abort();
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: { command: p.command, cwd: p.cwd, reason: p.reason },
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: { changes: p.changes, reason: p.reason },
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',