@loicngr/kobo 1.10.2 → 1.11.0

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 (234) hide show
  1. package/AGENTS.md +5 -4
  2. package/CHANGELOG.md +29 -0
  3. package/README.md +16 -10
  4. package/dist/server/db/migrations.js +21 -0
  5. package/dist/server/db/schema.js +14 -0
  6. package/dist/server/routes/health.js +2 -2
  7. package/dist/server/routes/notion.js +18 -1
  8. package/dist/server/routes/sentry.js +15 -1
  9. package/dist/server/routes/workspaces.js +140 -6
  10. package/dist/server/services/agent/engines/claude-code/engine.js +17 -1
  11. package/dist/server/services/agent/engines/claude-code/event-mapper.js +31 -2
  12. package/dist/server/services/agent/engines/claude-code/options-builder.js +3 -0
  13. package/dist/server/services/agent/engines/codex/engine.js +70 -8
  14. package/dist/server/services/agent/engines/codex/server-requests.js +13 -3
  15. package/dist/server/services/agent/engines/codex/turn-liveness.js +53 -0
  16. package/dist/server/services/agent/event-router.js +4 -0
  17. package/dist/server/services/agent/orchestrator.js +17 -0
  18. package/dist/server/services/agent/streaming-batcher.js +34 -0
  19. package/dist/server/services/notion-service.js +19 -0
  20. package/dist/server/services/pr-watcher-service.js +22 -17
  21. package/dist/server/services/search-service.js +3 -1
  22. package/dist/server/services/sentry-service.js +20 -0
  23. package/dist/server/services/settings-service.js +130 -1
  24. package/dist/server/services/workspace-permission-policy-service.js +86 -0
  25. package/dist/server/utils/paths.js +3 -3
  26. package/package.json +3 -3
  27. package/src/client/dist/pwa/assets/ActivityFeed-D9EfbRqv.css +1 -0
  28. package/src/client/dist/pwa/assets/ActivityFeed-ujsfk0sG.js +8 -0
  29. package/src/client/dist/{spa/assets/ChangelogPage-EA4LPGWB.js → pwa/assets/ChangelogPage-DLmP9KSb.js} +1 -1
  30. package/src/client/dist/{spa/assets/ClosePopup-BxYvEnmx.js → pwa/assets/ClosePopup-Cc7aUs96.js} +1 -1
  31. package/src/client/dist/pwa/assets/CreatePage-DfMhzgbv.js +2 -0
  32. package/src/client/dist/pwa/assets/CreatePage-qMgdzdb-.css +1 -0
  33. package/src/client/dist/pwa/assets/DiffViewer-B-TSPjtj.js +8 -0
  34. package/src/client/dist/{spa/assets/DiffViewer-BuYD1EDP.css → pwa/assets/DiffViewer-CRrDdYBa.css} +1 -1
  35. package/src/client/dist/{spa/assets/HealthPage-5F4m82-8.js → pwa/assets/HealthPage-13Ugeexp.js} +1 -1
  36. package/src/client/dist/pwa/assets/MainLayout-BzOcdtNd.js +37 -0
  37. package/src/client/dist/pwa/assets/MainLayout-CgHzEM37.css +1 -0
  38. package/src/client/dist/{spa/assets/QBtnToggle-Br3smB6b.js → pwa/assets/QBtnToggle-BUJ1g7BR.js} +1 -1
  39. package/src/client/dist/{spa/assets/QChip-Bc7L8LKe.js → pwa/assets/QChip-CPiNmK-x.js} +1 -1
  40. package/src/client/dist/pwa/assets/QSelect-D63npmWX.js +36 -0
  41. package/src/client/dist/pwa/assets/QTooltip-DOQ5d9M2.js +1 -0
  42. package/src/client/dist/pwa/assets/SearchPage-B08_UXDs.css +1 -0
  43. package/src/client/dist/pwa/assets/SearchPage-C7ulmpmI.js +1 -0
  44. package/src/client/dist/pwa/assets/SettingsPage-DJEH2ahl.css +1 -0
  45. package/src/client/dist/pwa/assets/SettingsPage-DY1g-LTs.js +16 -0
  46. package/src/client/dist/pwa/assets/ThinkingItem-BVoamjnK.css +1 -0
  47. package/src/client/dist/pwa/assets/ThinkingItem-M7hXIRih.js +1 -0
  48. package/src/client/dist/{spa/assets/TouchPan-IMQRiAGn.js → pwa/assets/TouchPan-g-ZGD0pd.js} +1 -1
  49. package/src/client/dist/pwa/assets/WorkspacePage-BU_uwlKN.js +4 -0
  50. package/src/client/dist/pwa/assets/WorkspacePage-CnRbNBAh.css +1 -0
  51. package/src/client/dist/{spa/assets/build-path-tree-Cgbihluh.js → pwa/assets/build-path-tree-B3R74R5X.js} +1 -1
  52. package/src/client/dist/{spa/assets/cssMode-57i-GAo-.js → pwa/assets/cssMode-Ca0QWQGQ.js} +1 -1
  53. package/src/client/dist/pwa/assets/documents-a-MDhFWk.js +1 -0
  54. package/src/client/dist/{spa/assets/editor.api2-CNFQDxW4.js → pwa/assets/editor.api2-CQQ3TFHp.js} +1 -1
  55. package/src/client/dist/{spa/assets/editor.main-CJJlXa_x.js → pwa/assets/editor.main-BmtlNqLW.js} +2 -2
  56. package/src/client/dist/{spa/assets/expand-template-CN8wfDhv.js → pwa/assets/expand-template-DkLDjInO.js} +1 -1
  57. package/src/client/dist/{spa/assets/formatters-9eR6rvkm.js → pwa/assets/formatters-COWwqgNl.js} +1 -1
  58. package/src/client/dist/{spa/assets/freemarker2-qbKTPHxB.js → pwa/assets/freemarker2-Be_VI-2P.js} +1 -1
  59. package/src/client/dist/{spa/assets/handlebars-DV81bryH.js → pwa/assets/handlebars-C6MV-4UG.js} +1 -1
  60. package/src/client/dist/{spa/assets/html-i5i_Ua1s.js → pwa/assets/html-H42ZDxQV.js} +1 -1
  61. package/src/client/dist/{spa/assets/htmlMode-Du1z3aXL.js → pwa/assets/htmlMode-COCELcr-.js} +1 -1
  62. package/src/client/dist/pwa/assets/i18n-BgrVh81z.js +1 -0
  63. package/src/client/dist/pwa/assets/index-Cs6I8vi_.js +84 -0
  64. package/src/client/dist/{spa/assets/javascript-DgQZF_pf.js → pwa/assets/javascript-CiFkTKAE.js} +1 -1
  65. package/src/client/dist/{spa/assets/jsonMode-6XLEI_p0.js → pwa/assets/jsonMode--0Mpy1sQ.js} +1 -1
  66. package/src/client/dist/{spa/assets/kobo-commands-CplA4Y0q.js → pwa/assets/kobo-commands-BzxwZ3vQ.js} +1 -1
  67. package/src/client/dist/{spa/assets/layout-BbHOhk37.js → pwa/assets/layout-BcUfiaFQ.js} +1 -1
  68. package/src/client/dist/{spa/assets/liquid-54MmW5iG.js → pwa/assets/liquid-BlkSsRl8.js} +1 -1
  69. package/src/client/dist/{spa/assets/lspLanguageFeatures-xU1LF4fY.js → pwa/assets/lspLanguageFeatures-DXelvPSE.js} +1 -1
  70. package/src/client/dist/{spa/assets/mdx-BbX1cCc8.js → pwa/assets/mdx-BoNBdGqW.js} +1 -1
  71. package/src/client/dist/{spa/assets/monaco.contribution-CNHTTcDF.js → pwa/assets/monaco.contribution-D8uREgcb.js} +2 -2
  72. package/src/client/dist/pwa/assets/network-auth-wzwG23Rr.js +1 -0
  73. package/src/client/dist/pwa/assets/pending-diff-open-DyoLDUlV.js +1 -0
  74. package/src/client/dist/pwa/assets/permissionModes-D7Iv04mQ.js +1 -0
  75. package/src/client/dist/{spa/assets/project-color-PpNx-yz4.js → pwa/assets/project-color-C3soNzD0.js} +1 -1
  76. package/src/client/dist/{spa/assets/python-BL4EBjGI.js → pwa/assets/python-DZCdvSIy.js} +1 -1
  77. package/src/client/dist/{spa/assets/razor-BdIRjBrX.js → pwa/assets/razor-CFwK0FwM.js} +1 -1
  78. package/src/client/dist/{spa/assets/render-chat-markdown-CYeqc8cI.js → pwa/assets/render-chat-markdown-DCydC_jc.js} +1 -1
  79. package/src/client/dist/{spa/assets/tsMode-BoQuq_mo.js → pwa/assets/tsMode-D8ZbVZB6.js} +1 -1
  80. package/src/client/dist/{spa/assets/typescript-TXE1oBSg.js → pwa/assets/typescript-CjxR71vN.js} +1 -1
  81. package/src/client/dist/{spa/assets/use-onboarding-Ch0swQ7p.js → pwa/assets/use-onboarding-DA5ka-XJ.js} +1 -1
  82. package/src/client/dist/{spa/assets/use-quasar--cg_cUkd.js → pwa/assets/use-quasar-5A_Wy1iR.js} +1 -1
  83. package/src/client/dist/{spa/assets/workers-Clrb2kWw.js → pwa/assets/workers-Bv1gqcaP.js} +1 -1
  84. package/src/client/dist/{spa/assets/xml-36_1aaai.js → pwa/assets/xml-vP8L_5ev.js} +1 -1
  85. package/src/client/dist/{spa/assets/yaml-CTVfk9Oc.js → pwa/assets/yaml-YN_D0NXJ.js} +1 -1
  86. package/src/client/dist/{spa → pwa}/index.html +2 -2
  87. package/src/client/dist/pwa/manifest.json +1 -0
  88. package/src/client/dist/pwa/sounds/7eme-compagnie-03.mp3 +0 -0
  89. package/src/client/dist/pwa/sounds/aller-ftg.mp3 +0 -0
  90. package/src/client/dist/pwa/sounds/arrete-de-mentir.mp3 +0 -0
  91. package/src/client/dist/pwa/sounds/arretez-les-messages.mp3 +0 -0
  92. package/src/client/dist/pwa/sounds/bah-alors-on-est-nul.mp3 +0 -0
  93. package/src/client/dist/pwa/sounds/gta-v-death.mp3 +0 -0
  94. package/src/client/dist/pwa/sounds/nan-tu-degages.mp3 +0 -0
  95. package/src/client/dist/pwa/sounds/nan-wallah-pardon.mp3 +0 -0
  96. package/src/client/dist/pwa/sounds/ouais-cest-greg.mp3 +0 -0
  97. package/src/client/dist/pwa/sounds/pas-ca-zinedine.mp3 +0 -0
  98. package/src/client/dist/pwa/sounds/ta-gueule.mp3 +0 -0
  99. package/src/client/dist/pwa/sounds/tu-vas-la-fermer.mp3 +0 -0
  100. package/src/client/dist/pwa/sw.js +1 -0
  101. package/src/client/dist/pwa/workbox-778a14ff.js +1 -0
  102. package/src/client/dist/spa/assets/ActivityFeed-98LfDiwH.js +0 -8
  103. package/src/client/dist/spa/assets/ActivityFeed-HbMZOClq.css +0 -1
  104. package/src/client/dist/spa/assets/CreatePage-C1yAhjsO.css +0 -1
  105. package/src/client/dist/spa/assets/CreatePage-Cg0BbQwp.js +0 -2
  106. package/src/client/dist/spa/assets/DiffViewer-CLp2N6Zh.js +0 -8
  107. package/src/client/dist/spa/assets/MainLayout-7nwhu8UE.css +0 -1
  108. package/src/client/dist/spa/assets/MainLayout-Dfc_DsIt.js +0 -37
  109. package/src/client/dist/spa/assets/QExpansionItem-BHyDyRL4.js +0 -1
  110. package/src/client/dist/spa/assets/QScrollArea-CncsSfK0.js +0 -1
  111. package/src/client/dist/spa/assets/QSelect-DXXOwpf6.js +0 -36
  112. package/src/client/dist/spa/assets/QTooltip-C6Vpr1qf.js +0 -1
  113. package/src/client/dist/spa/assets/SearchPage-BYM38aZA.js +0 -1
  114. package/src/client/dist/spa/assets/SearchPage-cVwt0DaQ.css +0 -1
  115. package/src/client/dist/spa/assets/SettingsPage-DendQ8y4.js +0 -16
  116. package/src/client/dist/spa/assets/SettingsPage-NDsw0Jvo.css +0 -1
  117. package/src/client/dist/spa/assets/WorkspacePage-BOApCawq.css +0 -1
  118. package/src/client/dist/spa/assets/WorkspacePage-BghKaAab.js +0 -4
  119. package/src/client/dist/spa/assets/documents-CdYfTEAV.js +0 -1
  120. package/src/client/dist/spa/assets/i18n-DA8dnYBE.js +0 -1
  121. package/src/client/dist/spa/assets/index-B7ivRaq9.js +0 -84
  122. package/src/client/dist/spa/assets/network-auth-CdzNfMI-.js +0 -1
  123. package/src/client/dist/spa/assets/permissionModes-6jhegAm-.js +0 -1
  124. /package/src/client/dist/{spa → pwa}/assets/ChangelogPage-CQ33An2f.css +0 -0
  125. /package/src/client/dist/{spa → pwa}/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuYjalmUiAw-BCOjigVR.woff +0 -0
  126. /package/src/client/dist/{spa → pwa}/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuZtalmUiAw-Ocp2tNig.woff +0 -0
  127. /package/src/client/dist/{spa → pwa}/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuaabVmUiAw-6jEn9xEl.woff +0 -0
  128. /package/src/client/dist/{spa → pwa}/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWub2bVmUiAw-CCs8I9zJ.woff +0 -0
  129. /package/src/client/dist/{spa → pwa}/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbFmUiAw-BLaboPi0.woff +0 -0
  130. /package/src/client/dist/{spa → pwa}/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAw-DHuK6FBl.woff +0 -0
  131. /package/src/client/dist/{spa → pwa}/assets/QBadge-DV5kn4mL.js +0 -0
  132. /package/src/client/dist/{spa → pwa}/assets/QBanner-CmfaeizG.js +0 -0
  133. /package/src/client/dist/{spa → pwa}/assets/QBtnGroup-C442-yBb.js +0 -0
  134. /package/src/client/dist/{spa → pwa}/assets/QIcon-C6E-oEjI.js +0 -0
  135. /package/src/client/dist/{spa → pwa}/assets/QInput-BF8x4RTv.js +0 -0
  136. /package/src/client/dist/{spa → pwa}/assets/QList-D5EKdFPn.js +0 -0
  137. /package/src/client/dist/{spa → pwa}/assets/QPage-D3rjwA1l.js +0 -0
  138. /package/src/client/dist/{spa → pwa}/assets/QSeparator-DWTCQxsa.js +0 -0
  139. /package/src/client/dist/{spa → pwa}/assets/QSpace-ezNrZkuj.js +0 -0
  140. /package/src/client/dist/{spa → pwa}/assets/QSpinnerDots-DIV68-cV.js +0 -0
  141. /package/src/client/dist/{spa → pwa}/assets/_plugin-vue_export-helper-BDNMzG2s.js +0 -0
  142. /package/src/client/dist/{spa → pwa}/assets/abap-08VXUWAP.js +0 -0
  143. /package/src/client/dist/{spa → pwa}/assets/apex-BWPQTe0t.js +0 -0
  144. /package/src/client/dist/{spa → pwa}/assets/azcli-Bc_sGQ0U.js +0 -0
  145. /package/src/client/dist/{spa → pwa}/assets/bat-i0X4ZdIN.js +0 -0
  146. /package/src/client/dist/{spa → pwa}/assets/bicep-B5-_aFwp.js +0 -0
  147. /package/src/client/dist/{spa → pwa}/assets/cameligo-DMUM7wLl.js +0 -0
  148. /package/src/client/dist/{spa → pwa}/assets/chunk-QTnfLwEv.js +0 -0
  149. /package/src/client/dist/{spa → pwa}/assets/clojure-Cm7r79vr.js +0 -0
  150. /package/src/client/dist/{spa → pwa}/assets/codicon-ngg6Pgfi.ttf +0 -0
  151. /package/src/client/dist/{spa → pwa}/assets/coffee-Ba7i2nA0.js +0 -0
  152. /package/src/client/dist/{spa → pwa}/assets/cpp-C7h46wYY.js +0 -0
  153. /package/src/client/dist/{spa → pwa}/assets/csharp-BKxtCVv1.js +0 -0
  154. /package/src/client/dist/{spa → pwa}/assets/csp-bTuwJoIa.js +0 -0
  155. /package/src/client/dist/{spa → pwa}/assets/css-DIMkf-bt.js +0 -0
  156. /package/src/client/dist/{spa → pwa}/assets/css.worker-CBlhdqTa.js +0 -0
  157. /package/src/client/dist/{spa → pwa}/assets/cypher-CVaqCwHa.js +0 -0
  158. /package/src/client/dist/{spa → pwa}/assets/dart-onAF5SnQ.js +0 -0
  159. /package/src/client/dist/{spa → pwa}/assets/dockerfile-DZFCIeNp.js +0 -0
  160. /package/src/client/dist/{spa → pwa}/assets/ecl-D05T4iGw.js +0 -0
  161. /package/src/client/dist/{spa → pwa}/assets/editor-BFebQ78e.css +0 -0
  162. /package/src/client/dist/{spa → pwa}/assets/editor.worker-Cu3tR8iJ.js +0 -0
  163. /package/src/client/dist/{spa → pwa}/assets/elixir-6RTg0lbw.js +0 -0
  164. /package/src/client/dist/{spa → pwa}/assets/expand-template-D2yUa54D.css +0 -0
  165. /package/src/client/dist/{spa → pwa}/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNa-Dr0goTwe.woff +0 -0
  166. /package/src/client/dist/{spa → pwa}/assets/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ-D-x-0Q06.woff2 +0 -0
  167. /package/src/client/dist/{spa → pwa}/assets/flow9-C5_-GSwl.js +0 -0
  168. /package/src/client/dist/{spa → pwa}/assets/fsharp-C8Ef5oNN.js +0 -0
  169. /package/src/client/dist/{spa → pwa}/assets/go-C-y9NEjX.js +0 -0
  170. /package/src/client/dist/{spa → pwa}/assets/graphql-fmXr3nnJ.js +0 -0
  171. /package/src/client/dist/{spa → pwa}/assets/hcl-CpzslTdj.js +0 -0
  172. /package/src/client/dist/{spa → pwa}/assets/html.worker-BKBrNna1.js +0 -0
  173. /package/src/client/dist/{spa → pwa}/assets/index-CwYR3iHt.css +0 -0
  174. /package/src/client/dist/{spa → pwa}/assets/ini-sBoK_t0W.js +0 -0
  175. /package/src/client/dist/{spa → pwa}/assets/java-BEtHBSE6.js +0 -0
  176. /package/src/client/dist/{spa → pwa}/assets/json.worker-BaLYt9Ss.js +0 -0
  177. /package/src/client/dist/{spa → pwa}/assets/julia-Bri6UV-V.js +0 -0
  178. /package/src/client/dist/{spa → pwa}/assets/kotlin-BOotOW0E.js +0 -0
  179. /package/src/client/dist/{spa → pwa}/assets/less-B9JPFI3C.js +0 -0
  180. /package/src/client/dist/{spa → pwa}/assets/lexon-CfSJPG6W.js +0 -0
  181. /package/src/client/dist/{spa → pwa}/assets/lua-CsQS60Ue.js +0 -0
  182. /package/src/client/dist/{spa → pwa}/assets/m3-D-oSqn_W.js +0 -0
  183. /package/src/client/dist/{spa → pwa}/assets/markdown-Cimd5fb3.js +0 -0
  184. /package/src/client/dist/{spa → pwa}/assets/mips-CIPQ_RoX.js +0 -0
  185. /package/src/client/dist/{spa → pwa}/assets/msdax-DauUninz.js +0 -0
  186. /package/src/client/dist/{spa → pwa}/assets/mysql-SOo6toE5.js +0 -0
  187. /package/src/client/dist/{spa → pwa}/assets/objective-c-FvmIjYaQ.js +0 -0
  188. /package/src/client/dist/{spa → pwa}/assets/pascal-DrH0SRf2.js +0 -0
  189. /package/src/client/dist/{spa → pwa}/assets/pascaligo-D-ptJ9y-.js +0 -0
  190. /package/src/client/dist/{spa → pwa}/assets/perl-oz_6vUea.js +0 -0
  191. /package/src/client/dist/{spa → pwa}/assets/pgsql-DTj74zXo.js +0 -0
  192. /package/src/client/dist/{spa → pwa}/assets/php-nr791fC2.js +0 -0
  193. /package/src/client/dist/{spa → pwa}/assets/pla-CopQ2nXW.js +0 -0
  194. /package/src/client/dist/{spa → pwa}/assets/postiats-43DmfD33.js +0 -0
  195. /package/src/client/dist/{spa → pwa}/assets/powerquery-D3hlyOfw.js +0 -0
  196. /package/src/client/dist/{spa → pwa}/assets/powershell-DmHpPYUd.js +0 -0
  197. /package/src/client/dist/{spa → pwa}/assets/protobuf-C531GsRP.js +0 -0
  198. /package/src/client/dist/{spa → pwa}/assets/pug-Z5eAx3Zn.js +0 -0
  199. /package/src/client/dist/{spa → pwa}/assets/qsharp-DkqhCAOL.js +0 -0
  200. /package/src/client/dist/{spa → pwa}/assets/r-BwWrilGY.js +0 -0
  201. /package/src/client/dist/{spa → pwa}/assets/redis-ClamHrr6.js +0 -0
  202. /package/src/client/dist/{spa → pwa}/assets/redshift-DT7zqm-g.js +0 -0
  203. /package/src/client/dist/{spa → pwa}/assets/restructuredtext-BYgofb2h.js +0 -0
  204. /package/src/client/dist/{spa → pwa}/assets/ruby-DezsRK8O.js +0 -0
  205. /package/src/client/dist/{spa → pwa}/assets/rust-DdL9SqIa.js +0 -0
  206. /package/src/client/dist/{spa → pwa}/assets/sb-CcwsVR0C.js +0 -0
  207. /package/src/client/dist/{spa → pwa}/assets/scala-DHpiXF5c.js +0 -0
  208. /package/src/client/dist/{spa → pwa}/assets/scheme-BeGwcela.js +0 -0
  209. /package/src/client/dist/{spa → pwa}/assets/scss-gp-XZpBa.js +0 -0
  210. /package/src/client/dist/{spa → pwa}/assets/shell-CC2rA5mh.js +0 -0
  211. /package/src/client/dist/{spa → pwa}/assets/solidity-BEEn4gHE.js +0 -0
  212. /package/src/client/dist/{spa → pwa}/assets/sophia-CRfGWb83.js +0 -0
  213. /package/src/client/dist/{spa → pwa}/assets/sparql-D_Lu-MrJ.js +0 -0
  214. /package/src/client/dist/{spa → pwa}/assets/sql-NEE52Syq.js +0 -0
  215. /package/src/client/dist/{spa → pwa}/assets/st-DbInun42.js +0 -0
  216. /package/src/client/dist/{spa → pwa}/assets/swift-Bxkupp3x.js +0 -0
  217. /package/src/client/dist/{spa → pwa}/assets/systemverilog-Bz4Y3fRF.js +0 -0
  218. /package/src/client/dist/{spa → pwa}/assets/tcl-DISqw1ZD.js +0 -0
  219. /package/src/client/dist/{spa → pwa}/assets/ts.worker-PmaSgaZk.js +0 -0
  220. /package/src/client/dist/{spa → pwa}/assets/twig-De2hgUGE.js +0 -0
  221. /package/src/client/dist/{spa → pwa}/assets/typespec-B8J7ngcE.js +0 -0
  222. /package/src/client/dist/{spa → pwa}/assets/use-checkbox-E9SV7iH5.js +0 -0
  223. /package/src/client/dist/{spa → pwa}/assets/use-onboarding-B3l7mx48.css +0 -0
  224. /package/src/client/dist/{spa → pwa}/assets/vb-DV3o63ZY.js +0 -0
  225. /package/src/client/dist/{spa → pwa}/assets/vue.runtime.esm-bundler-JZnIeD9D.js +0 -0
  226. /package/src/client/dist/{spa → pwa}/assets/wgsl-DpFanUEy.js +0 -0
  227. /package/src/client/dist/{spa → pwa}/favicon.svg +0 -0
  228. /package/src/client/dist/{spa → pwa}/sounds/ca_va_peter.mp3 +0 -0
  229. /package/src/client/dist/{spa → pwa}/sounds/dry-fart.mp3 +0 -0
  230. /package/src/client/dist/{spa → pwa}/sounds/faaah.mp3 +0 -0
  231. /package/src/client/dist/{spa → pwa}/sounds/for-shure.mp3 +0 -0
  232. /package/src/client/dist/{spa → pwa}/sounds/hey.mp3 +0 -0
  233. /package/src/client/dist/{spa → pwa}/sounds/travail_termine.mp3 +0 -0
  234. /package/src/client/dist/{spa → pwa}/sounds/warcraft-3-humain-travail.mp3 +0 -0
package/AGENTS.md CHANGED
@@ -12,13 +12,13 @@ Single-user dev tool, local by default. The server binds `127.0.0.1` unless the
12
12
 
13
13
  **Backend**: Node.js ≥ 20, Hono (HTTP), `ws` (WebSocket), better-sqlite3 (WAL mode), nanoid, `@modelcontextprotocol/sdk`. TypeScript throughout, `tsx` for dev, `tsc` for production build.
14
14
 
15
- **Frontend**: Vue 3, Quasar 2, Pinia, vue-router, marked + dompurify for markdown rendering. Vite via `@quasar/app-vite`.
15
+ **Frontend**: Vue 3, Quasar 2, Pinia, vue-router, marked + dompurify for markdown rendering. Vite via `@quasar/app-vite`, built and served as a Progressive Web App in production.
16
16
 
17
17
  **Database**: a single SQLite file under the **Kōbō home directory** (`~/.config/kobo/kobo.db` by default, overridable via `KOBO_HOME`). Fresh-install schema lives in `src/server/db/schema.ts` (`initSchema`); incremental migrations live in `src/server/db/migrations.ts`. **The project is in production**, so every schema change MUST ship as a migration that preserves data, never as a breaking change to `initSchema` alone. See [Database migrations](#database-migrations) below.
18
18
 
19
19
  **Kōbō home directory**: `KOBO_HOME` env var overrides everything. Otherwise `$XDG_CONFIG_HOME/kobo/`, else `~/.config/kobo/`. Contains `kobo.db`, `settings.json`, `skills.json`, `templates.json`. **Development uses `./data/`** via the `KOBO_HOME=./data` prefix in the `dev` npm script, so local dev never touches your real `~/.config/kobo/` and can run in parallel with a production-installed Kōbō (`npx @loicngr/kobo`). See `src/server/utils/paths.ts`.
20
20
 
21
- **Tests**: vitest (24 backend test files, 544+ tests; 5 frontend test files, 45+ tests at time of writing). Frontend tests cover Pinia stores and pure utility modules; Vue components are not tested (type-check + manual smoke only).
21
+ **Tests**: Vitest covers backend services/routes and frontend stores/pure utilities. Vue components are not unit-tested; type-checking and manual smoke tests cover their behavior. Run `npm test` and `(cd src/client && npm test)` for the complete suites.
22
22
 
23
23
  ## Commands
24
24
 
@@ -83,7 +83,7 @@ src/
83
83
  │ ├── utils/
84
84
  │ │ ├── git-ops.ts # pushBranch, pullBranch, getCommitsBetween, delete{Local,Remote}Branch…
85
85
  │ │ └── process-tracker.ts # per-workspace spawned-process map
86
- ├── client/ # Vue 3 + Quasar SPA
86
+ ├── client/ # Vue 3 + Quasar PWA
87
87
  │ └── src/
88
88
  │ ├── stores/ # pinia: workspace, websocket, settings, dev-server, templates
89
89
  │ ├── components/ # WorkspaceList, NotionPanel, AcceptancePanel, ChatInput, GitPanel, PlansPanel, WorkspaceAttentionLabels…
@@ -102,10 +102,11 @@ src/
102
102
  |---|---|
103
103
  | `workspaces` | the unit of work: id, name, project_path, source_branch, working_branch, status, notion_url, model, `brainstorm_model`, dev_server_status, `archived_at`, `worktree_purged_at`, `worktree_purge_restore_data` (JSON), `auto_loop`, `auto_loop_ready`, `auto_loop_session_mode`, `no_progress_streak`, timestamps |
104
104
  | `tasks` | workspace sub-items: title, status, `is_acceptance_criterion`, sort_order; CASCADE DELETE on workspace |
105
- | `agent_sessions` | Claude Code CLI invocations: pid, `claude_session_id`, status, started_at, ended_at, `name` |
105
+ | `agent_sessions` | agent-engine sessions: pid where applicable, engine session id, status, timestamps, model, and name |
106
106
  | `ws_events` | persisted WebSocket events for replay on reconnect: type, payload, session_id, created_at |
107
107
  | `pending_wakeups` | one-row-per-workspace scheduler for the `ScheduleWakeup` tool: target_at (ISO UTC), prompt, reason; CASCADE DELETE on workspace |
108
108
  | `workspace_chat_history` | chat-input history per workspace: message text + `created_at`, ordered by autoincrement id, capped at 200 entries by the service; CASCADE DELETE on workspace |
109
+ | `workspace_permission_rules` | remembered per-workspace tool approvals, scoped to an exact operation or every invocation of a tool; CASCADE DELETE on workspace |
109
110
 
110
111
  `status` enum: `created | extracting | brainstorming | executing | completed | idle | error | quota`. Transitions are validated in `updateWorkspaceStatus` against `VALID_TRANSITIONS`.
111
112
 
package/CHANGELOG.md CHANGED
@@ -4,6 +4,35 @@ All notable changes to Kōbō are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/). Each release is an `## <version>`
5
5
  section — the in-app "What's new" dialog reads this file.
6
6
 
7
+ ## 1.11.0
8
+
9
+ - feat(pwa): ship the production client as an installable Progressive Web App.
10
+ - feat(chat): add `Ctrl+J` for a new line and `Ctrl+Enter` to force delivery of
11
+ a queued message while an agent is active.
12
+ - feat(chat): move the latest Claude/Codex reasoning into one live panel above
13
+ the chat input, with a global visibility preference.
14
+ - feat(integrations): independently enable or disable Notion and Sentry from
15
+ Settings while preserving their configuration.
16
+ - feat(integrations): add **Test connection** checks for the configured Notion
17
+ and Sentry MCP servers, with actionable status and elapsed time.
18
+ - feat(chat): add `Ctrl+F` workspace-history search with readable results;
19
+ opening a result loads the matching session, scrolls to its message, and
20
+ highlights it. Global search now deep-links to the same location.
21
+ - feat(git): let Edit and Write cards open their exact worktree-relative file
22
+ in the Git diff viewer.
23
+ - feat(observability): add a session timeline panel and downloadable redacted
24
+ workspace diagnostic JSON.
25
+ - feat(pwa): show offline and update-ready states, with explicit update reload
26
+ instead of interrupting an active page.
27
+ - fix(chat): consume one-shot search deep-link parameters after use so a page
28
+ refresh does not jump back to an old result.
29
+ - feat(permissions): remember workspace-scoped tool or exact-operation
30
+ approvals across sessions.
31
+
32
+ ## 1.10.3
33
+
34
+ - feat(notifications): add configurable PR state sounds (#22)
35
+
7
36
  ## 1.10.2
8
37
 
9
38
  - feat(notifications): add workspace creation sound (#21)
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  [![license](https://img.shields.io/npm/l/@loicngr/kobo.svg)](./LICENSE)
7
7
  [![node](https://img.shields.io/node/v/@loicngr/kobo.svg)](https://nodejs.org/)
8
8
 
9
- Kōbō runs multiple coding agents in parallel, each isolated in its own git worktree, branch, and dev server. A single Vue dashboard streams output, tasks, git state, and quota usage across every workspace.
9
+ Kōbō runs multiple coding agents in parallel, each isolated in its own git worktree, branch, and dev server. Its installable Vue/Quasar dashboard streams output, tasks, git state, and quota usage across every workspace.
10
10
 
11
11
  ![Kōbō workspace view: live chat and git panel](docs/assets/images/workspace-chat.png)
12
12
 
@@ -17,15 +17,15 @@ Kōbō runs multiple coding agents in parallel, each isolated in its own git wor
17
17
 
18
18
  - **Isolated worktrees**: each workspace is a dedicated git worktree on its own branch, so parallel sessions never collide.
19
19
  - **Two agent engines**: Claude Code (via `@anthropic-ai/claude-agent-sdk`) and OpenAI Codex (via `codex app-server`), chosen per workspace.
20
- - **Live chat**: streaming text, reasoning blocks, inline Edit/Write diffs, per-turn cards, a compaction-in-progress indicator, infinite scrollback. `/` autocompletes skills and commands, `@` fuzzy-autocompletes worktree file paths, and you can export any workspace's session events to CSV. Messages queued while an agent is busy stay scoped to their workspace and session. **Send now** delivers a queued message to an active Claude Code stream or steers the active Codex turn; it remains queued until the engine confirms acceptance.
20
+ - **Live chat**: streaming text, inline Edit/Write diffs, per-turn cards, a compaction-in-progress indicator, infinite scrollback, and the latest Claude or Codex reasoning in a panel immediately above the message input. `/` autocompletes skills and commands, `@` fuzzy-autocompletes worktree file paths, and `Ctrl+F` searches readable messages in the current workspace: selecting a result loads its session and scrolls to it. The global search page deep-links to the same location. Messages queued while an agent is busy stay scoped to their workspace and session. **Send now** delivers a queued message to an active Claude Code stream or steers the active Codex turn; it remains queued until the engine confirms acceptance. Use `Enter` to send, `Ctrl+Enter` to force delivery while an agent is busy, and `Shift+Enter` or `Ctrl+J` for a new line.
21
21
  - **Full MCP toolset (`kobo-tasks`)**: a per-workspace MCP server the agent uses for far more than tasks — task/acceptance-criteria CRUD, starting/stopping the dev server and reading its logs, a unified `get_ticket` (Notion or Sentry), searching past conversations across every workspace, per-session token/cost usage, and a `.ai/thoughts` decision log. Native Claude Code Task tools complement it for lightweight sub-agent coordination. See [`AGENTS.md`](./AGENTS.md) for the full tool list.
22
22
 
23
23
  ![Sub-agents panel showing parallel tool calls](docs/assets/images/sub-agents-panel.png)
24
- - **Git panel**: a Monaco-based diff viewer with **inline file editing** (edit the right-hand panel directly, save with `Ctrl/Cmd+S`, conflict-guarded via sha precondition), inline conflict resolution, and `Sync` / `Push` / `Open PR` / `Change PR base` / `Change source branch`. Multi-forge: GitHub (`gh`), GitLab (`glab`), or no forge, auto-detected from the remote and overridable per project.
24
+ - **Git panel**: a Monaco-based diff viewer with **inline file editing** (edit the right-hand panel directly, save with `Ctrl/Cmd+S`, conflict-guarded via sha precondition), inline conflict resolution, and `Sync` / `Push` / `Open PR` / `Change PR base` / `Change source branch`. Edit/Write cards in chat open their exact changed file in the diff viewer. Multi-forge: GitHub (`gh`), GitLab (`glab`), or no forge, auto-detected from the remote and overridable per project.
25
25
 
26
26
  ![Diff viewer with side-by-side changes](docs/assets/images/diff-viewer.png)
27
27
  - **Dev server panel**: start, stop, and tail logs for a workspace's dev server (Docker or npm) straight from the Tools panel — no need to leave the UI.
28
- - **Attention indicators**: workspace cards surface CI failures, review-requested changes, and a conflict-aware **ready-to-merge** badge, plus a one-click **Fix CI** button that spawns a fix workspace automatically.
28
+ - **Attention indicators**: workspace cards surface CI failures, review-requested changes, and a conflict-aware **ready-to-merge** badge, plus a one-click **Fix CI** button that spawns a fix workspace automatically. Seven PR/MR and CI transitions each have their own audio toggle, sound, and volume; simultaneous transitions play sequentially.
29
29
  - **Interactive Q&A**: an agent can pause mid-session to ask a clarifying question through the UI; the workspace surfaces under "Needs Attention" until you answer. Choosing **Other** opens an inline free-form field, and option previews are available on hover. Codex native interactive questions require its `plan` permission mode.
30
30
 
31
31
  ![Agent asking a clarifying question, awaiting the user's answer](docs/assets/images/agent-question.png)
@@ -35,7 +35,9 @@ Kōbō runs multiple coding agents in parallel, each isolated in its own git wor
35
35
  - **Cron schedules**: recurring per-workspace triggers the agent registers through MCP tools (`cron_create` / `cron_delete` / `cron_list`). Each tick resumes the workspace session (skipped if already active), and schedules are re-armed at boot with skip-missed semantics.
36
36
  - **Lifecycle scripts**: shell scripts run automatically at key moments — **setup** (worktree created), **cleanup** (session ended), **archive** (workspace archived). Configure them globally or per project, with their output streamed into the chat.
37
37
  - **Disk-space purge**: free a merged workspace's disk space without losing its chat history — see [below](#disk-space-purge).
38
- - **Optional integrations**: Notion (import missions), Sentry (fix from issue URL), local voice transcription (whisper.cpp).
38
+ - **Observability**: a compact session timeline in the right panel shows session duration and status; download a redacted workspace diagnostic JSON for troubleshooting.
39
+ - **Optional integrations**: independently enable or disable Notion (import missions) and Sentry (fix from issue URL) in Settings, with a **Test connection** action for each; local voice transcription uses whisper.cpp.
40
+ - **Installable PWA**: production builds work as a Progressive Web App. Install Kōbō from your browser on desktop or mobile for an app-like window; it signals offline state and offers an explicit reload when an update is ready. Live agent activity still requires a connection to the local Kōbō server.
39
41
 
40
42
  ## Quick start
41
43
 
@@ -53,6 +55,10 @@ SERVER_PORT=9997 PORT=9998 npx @loicngr/kobo@latest
53
55
 
54
56
  Open <http://localhost:3000> (or whichever port you picked). Data is persisted under `~/.config/kobo/` (override via `KOBO_HOME`).
55
57
 
58
+ ### Install as an app
59
+
60
+ Kōbō's production build is a PWA. When opening a production instance, use your browser's **Install app** / **Add to Home Screen** action to install it. It shows an offline notice and lets you explicitly reload when an update is ready, so an active session is never interrupted automatically. The development client (`npm run dev:client`) intentionally runs as a normal Vite application, so it does not expose an install prompt.
61
+
56
62
  Want to run from source or contribute? See [`CONTRIBUTING.md`](./CONTRIBUTING.md).
57
63
 
58
64
  ### Docker
@@ -78,7 +84,7 @@ The most common knobs:
78
84
  | `ANTHROPIC_API_KEY` | none | Claude Code engine credential (alternative to `claude /login`) |
79
85
  | `OPENAI_API_KEY` | none | Codex engine credential (alternative to `codex login`) |
80
86
 
81
- Global and per-project settings (worktree path, dev server commands, E2E framework, prompt templates, git conventions, branch prefixes, lifecycle scripts, task prompt) are edited in **Settings** at runtime. Per-project values inherit from the global ones when left empty.
87
+ Global and per-project settings (worktree path, dev server commands, E2E framework, prompt templates, git conventions, branch prefixes, lifecycle scripts, task prompt) are edited in **Settings** at runtime. Notion and Sentry are enabled independently in their respective tabs; disabling one keeps its configuration and existing workspace links, but removes its import and automation UI. In **Settings → General → Activity feed**, you can hide agent reasoning panels. Per-project values inherit from the global ones when left empty.
82
88
 
83
89
  The full reference (every env var, every setting key, MCP server registration, Notion / Sentry / Voice setup) is in [`CONFIGURATION.md`](./CONFIGURATION.md).
84
90
 
@@ -102,8 +108,8 @@ A merged workspace is automatically archived, but its worktree folder usually ca
102
108
 
103
109
  Kōbō ships first-class support for three external systems. All are opt-in and reuse credentials you may already have configured for Claude Code.
104
110
 
105
- - **Notion**: import missions, tasks, and acceptance criteria from a Notion page.
106
- - **Sentry**: paste an issue URL to spawn a fix workspace with the stacktrace, tags, and a TDD workflow.
111
+ - **Notion**: import missions, tasks, and acceptance criteria from a Notion page. Enable it in **Settings → Notion** when you want its creation/import flow, then use **Test connection** to validate its MCP credentials.
112
+ - **Sentry**: paste an issue URL to spawn a fix workspace with the stacktrace, tags, and a TDD workflow. Enable it independently in **Settings → Sentry**; **Test connection** validates its MCP credentials and authenticated identity.
107
113
  - **Voice transcription**: local push-to-talk via [`whisper.cpp`](https://github.com/ggml-org/whisper.cpp).
108
114
 
109
115
  See [`CONFIGURATION.md`](./CONFIGURATION.md) for the setup of each.
@@ -145,14 +151,14 @@ Full install instructions and the prompt-suite differences are in [`CONFIGURATIO
145
151
 
146
152
  ## Architecture
147
153
 
148
- Hono backend, Vue 3 + Quasar SPA, SQLite (WAL) for persistence, WebSocket for live updates. Each workspace spawns its own agent engine and a dedicated MCP server (`kobo-tasks`) that the agent uses to query and mutate workspace state.
154
+ Hono backend, Vue 3 + Quasar PWA, SQLite (WAL) for persistence, WebSocket for live updates. Each workspace spawns its own agent engine and a dedicated MCP server (`kobo-tasks`) that the agent uses to query and mutate workspace state.
149
155
 
150
156
  ```
151
157
  src/
152
158
  ├── server/ # Hono backend (routes, services, db, agent orchestrator)
153
159
  │ ├── services/agent/engines/ # claude-code/ + codex/ engines
154
160
  │ └── ...
155
- ├── client/ # Vue 3 + Quasar SPA
161
+ ├── client/ # Vue 3 + Quasar PWA
156
162
  ├── mcp-server/ # kobo-tasks MCP server, spawned per workspace
157
163
  ├── shared/ # types shared backend ↔ frontend
158
164
  └── __tests__/ # Vitest suite, backend + client, thousands of tests
@@ -420,6 +420,27 @@ export const migrations = [
420
420
  }
421
421
  },
422
422
  },
423
+ {
424
+ version: 32,
425
+ name: 'add-workspace-permission-rules',
426
+ migrate: (db) => {
427
+ db.exec(`
428
+ CREATE TABLE IF NOT EXISTS workspace_permission_rules (
429
+ id TEXT PRIMARY KEY,
430
+ workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
431
+ engine TEXT,
432
+ tool_name TEXT NOT NULL,
433
+ scope TEXT NOT NULL CHECK(scope IN ('operation', 'tool')),
434
+ fingerprint TEXT,
435
+ display_label TEXT NOT NULL,
436
+ created_at TEXT NOT NULL,
437
+ updated_at TEXT NOT NULL
438
+ );
439
+ CREATE INDEX IF NOT EXISTS idx_workspace_permission_rules_lookup
440
+ ON workspace_permission_rules(workspace_id, engine, tool_name, scope, fingerprint);
441
+ `);
442
+ },
443
+ },
423
444
  ];
424
445
  /** Current schema version — always equals the highest migration version. */
425
446
  export const SCHEMA_VERSION = migrations.length > 0 ? migrations[migrations.length - 1].version : 1;
@@ -73,6 +73,20 @@ export function initSchema(db) {
73
73
  created_at TEXT NOT NULL
74
74
  );
75
75
 
76
+ CREATE TABLE IF NOT EXISTS workspace_permission_rules (
77
+ id TEXT PRIMARY KEY,
78
+ workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
79
+ engine TEXT,
80
+ tool_name TEXT NOT NULL,
81
+ scope TEXT NOT NULL CHECK(scope IN ('operation', 'tool')),
82
+ fingerprint TEXT,
83
+ display_label TEXT NOT NULL,
84
+ created_at TEXT NOT NULL,
85
+ updated_at TEXT NOT NULL
86
+ );
87
+ CREATE INDEX IF NOT EXISTS idx_workspace_permission_rules_lookup
88
+ ON workspace_permission_rules(workspace_id, engine, tool_name, scope, fingerprint);
89
+
76
90
  CREATE TABLE IF NOT EXISTS pending_wakeups (
77
91
  workspace_id TEXT PRIMARY KEY REFERENCES workspaces(id) ON DELETE CASCADE,
78
92
  target_at TEXT NOT NULL,
@@ -164,8 +164,8 @@ app.get('/report', (c) => {
164
164
  },
165
165
  agentSessions: { orphaned },
166
166
  integrations: {
167
- notion: { configured: Boolean(healthGlobalSettings.notionMcpKey) },
168
- sentry: { configured: Boolean(healthGlobalSettings.sentryMcpKey) },
167
+ notion: { configured: Boolean(healthGlobalSettings.notionMcpKey), enabled: healthGlobalSettings.notionEnabled },
168
+ sentry: { configured: Boolean(healthGlobalSettings.sentryMcpKey), enabled: healthGlobalSettings.sentryEnabled },
169
169
  editor: { configured: Boolean(healthGlobalSettings.editorCommand) },
170
170
  },
171
171
  active: {
@@ -1,10 +1,24 @@
1
1
  import { Hono } from 'hono';
2
- import { extractNotionPage, listNotionUsers } from '../services/notion-service.js';
2
+ import { extractNotionPage, listNotionUsers, testNotionConnection } from '../services/notion-service.js';
3
+ import { getGlobalSettings } from '../services/settings-service.js';
3
4
  /** Hono sub-router for Notion page extraction. */
4
5
  const app = new Hono();
6
+ app.post('/test', async (c) => {
7
+ try {
8
+ if (!getGlobalSettings().notionEnabled)
9
+ return c.json({ error: 'Notion integration is disabled in Settings' }, 403);
10
+ return c.json(await testNotionConnection());
11
+ }
12
+ catch (err) {
13
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
14
+ }
15
+ });
5
16
  // GET /api/notion/users — list workspace users (humans only, for the settings dropdown)
6
17
  app.get('/users', async (c) => {
7
18
  try {
19
+ if (!getGlobalSettings().notionEnabled) {
20
+ return c.json({ error: 'Notion integration is disabled in Settings' }, 403);
21
+ }
8
22
  const users = await listNotionUsers();
9
23
  return c.json({ users });
10
24
  }
@@ -16,6 +30,9 @@ app.get('/users', async (c) => {
16
30
  // POST /api/notion/extract — extract a Notion page
17
31
  app.post('/extract', async (c) => {
18
32
  try {
33
+ if (!getGlobalSettings().notionEnabled) {
34
+ return c.json({ error: 'Notion integration is disabled in Settings' }, 403);
35
+ }
19
36
  const body = await c.req.json();
20
37
  if (!body.url) {
21
38
  return c.json({ error: 'Missing required field: url' }, 400);
@@ -1,10 +1,24 @@
1
1
  import { Hono } from 'hono';
2
- import { extractSentryIssue } from '../services/sentry-service.js';
2
+ import { extractSentryIssue, testSentryConnection } from '../services/sentry-service.js';
3
+ import { getGlobalSettings } from '../services/settings-service.js';
3
4
  /** Hono sub-router for Sentry issue extraction (preflight). */
4
5
  const app = new Hono();
6
+ app.post('/test', async (c) => {
7
+ try {
8
+ if (!getGlobalSettings().sentryEnabled)
9
+ return c.json({ error: 'Sentry integration is disabled in Settings' }, 403);
10
+ return c.json(await testSentryConnection());
11
+ }
12
+ catch (err) {
13
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
14
+ }
15
+ });
5
16
  // POST /api/sentry/extract — extract a Sentry issue by URL
6
17
  app.post('/extract', async (c) => {
7
18
  try {
19
+ if (!getGlobalSettings().sentryEnabled) {
20
+ return c.json({ error: 'Sentry integration is disabled in Settings' }, 403);
21
+ }
8
22
  const body = await c.req.json();
9
23
  if (!body.url) {
10
24
  return c.json({ error: 'Missing required field: url' }, 400);
@@ -34,6 +34,7 @@ import { getSuitePrompts } from '../services/skill-suite-prompts.js';
34
34
  import * as terminalService from '../services/terminal-service.js';
35
35
  import * as wakeupService from '../services/wakeup-service.js';
36
36
  import * as wsService from '../services/websocket-service.js';
37
+ import * as permissionPolicyService from '../services/workspace-permission-policy-service.js';
37
38
  import * as workspaceService from '../services/workspace-service.js';
38
39
  import * as purgeWorktreeService from '../services/worktree-purge-service.js';
39
40
  import * as worktreeService from '../services/worktree-service.js';
@@ -181,6 +182,9 @@ app.post('/', migrationGuard, async (c) => {
181
182
  // before createWorkspace and surface failures as 422.
182
183
  let notionContent = null;
183
184
  if (body.notionUrl) {
185
+ if (!settingsService.getGlobalSettings().notionEnabled) {
186
+ return c.json({ error: 'Notion integration is disabled in Settings' }, 403);
187
+ }
184
188
  try {
185
189
  notionContent = await notionService.extractNotionPage(body.notionUrl);
186
190
  }
@@ -203,6 +207,9 @@ app.post('/', migrationGuard, async (c) => {
203
207
  }
204
208
  let sentryContent = null;
205
209
  if (body.sentryUrl) {
210
+ if (!settingsService.getGlobalSettings().sentryEnabled) {
211
+ return c.json({ error: 'Sentry integration is disabled in Settings' }, 403);
212
+ }
206
213
  try {
207
214
  sentryContent = await sentryService.extractSentryIssue(body.sentryUrl);
208
215
  }
@@ -376,11 +383,9 @@ app.post('/', migrationGuard, async (c) => {
376
383
  }
377
384
  // Update workspace name with Notion page title only if user didn't
378
385
  // provide a custom name. Prefix with the Notion unique-id (e.g.
379
- // "TK-123 | …") when the page has one — it makes the workspace
380
- // immediately scannable in the sidebar.
386
+ // Use the Notion page title when the user left the generic placeholder.
381
387
  if (notionContent.title && workspace.name === 'workspace') {
382
- const prefix = notionContent.ticketId ? `${notionContent.ticketId} | ` : '';
383
- workspace = workspaceService.updateWorkspaceName(workspace.id, `${prefix}${notionContent.title}`);
388
+ workspace = workspaceService.updateWorkspaceName(workspace.id, notionContent.title);
384
389
  }
385
390
  }
386
391
  // Create manual tasks/criteria if no Notion content was extracted
@@ -1275,10 +1280,14 @@ app.post('/:id/deferred-permission/decision', async (c) => {
1275
1280
  if (body.decision !== 'allow' && body.decision !== 'deny') {
1276
1281
  return c.json({ error: "decision must be 'allow' or 'deny'" }, 400);
1277
1282
  }
1283
+ if (body.scope && !['once', 'turn', 'operation', 'tool'].includes(body.scope)) {
1284
+ return c.json({ error: "scope must be 'once', 'turn', 'operation' or 'tool'" }, 400);
1285
+ }
1278
1286
  await agentManager.answerPendingPermission(id, {
1279
1287
  toolCallId: body.toolCallId,
1280
1288
  decision: body.decision,
1281
1289
  reason: typeof body.reason === 'string' ? body.reason : undefined,
1290
+ scope: body.scope,
1282
1291
  });
1283
1292
  return c.json({ ok: true });
1284
1293
  }
@@ -1288,6 +1297,20 @@ app.post('/:id/deferred-permission/decision', async (c) => {
1288
1297
  return c.json({ error: message }, status);
1289
1298
  }
1290
1299
  });
1300
+ app.get('/:id/permission-rules', (c) => {
1301
+ const id = c.req.param('id');
1302
+ if (!workspaceService.getWorkspace(id))
1303
+ return c.json({ error: `Workspace '${id}' not found` }, 404);
1304
+ return c.json({ rules: permissionPolicyService.listWorkspacePermissionRules(id) });
1305
+ });
1306
+ app.delete('/:id/permission-rules/:ruleId', (c) => {
1307
+ const id = c.req.param('id');
1308
+ if (!workspaceService.getWorkspace(id))
1309
+ return c.json({ error: `Workspace '${id}' not found` }, 404);
1310
+ if (!permissionPolicyService.removeWorkspacePermissionRule(id, c.req.param('ruleId')))
1311
+ return c.json({ error: 'Permission rule not found' }, 404);
1312
+ return c.json({ ok: true });
1313
+ });
1291
1314
  // DELETE /api/workspaces/:id/events/:eventId — permanently dismiss a single
1292
1315
  // persisted ws_events row (used today by the agent error banner so a closed
1293
1316
  // error doesn't replay on F5 / reconnect). Defensive: only deletes if the row
@@ -1465,6 +1488,103 @@ app.post('/:id/tasks/:taskId/notify-done', (c) => {
1465
1488
  return c.json({ error: message }, 500);
1466
1489
  }
1467
1490
  });
1491
+ // GET /api/workspaces/:id/history-search — full-workspace persisted conversation search.
1492
+ app.get('/:id/history-search', (c) => {
1493
+ try {
1494
+ const id = c.req.param('id');
1495
+ if (!workspaceService.getWorkspace(id))
1496
+ return c.json({ error: `Workspace '${id}' not found` }, 404);
1497
+ const query = (c.req.query('q') ?? '').trim();
1498
+ if (query.length < 2)
1499
+ return c.json({ results: [] });
1500
+ const limit = Math.min(parseInt(c.req.query('limit') ?? '30', 10) || 30, 100);
1501
+ const needle = `%${query.replace(/[%_]/g, '\\$&')}%`;
1502
+ const rows = getDb()
1503
+ .prepare(`SELECT id, session_id, type, payload, created_at FROM ws_events
1504
+ WHERE workspace_id = ? AND payload LIKE ? ESCAPE '\\'
1505
+ ORDER BY rowid DESC LIMIT ?`)
1506
+ .all(id, needle, limit * 4);
1507
+ const normalized = rows.flatMap((row) => {
1508
+ const result = readableHistorySearchResult(row.type, row.payload, query);
1509
+ if (!result)
1510
+ return [];
1511
+ return [{ eventId: row.id, sessionId: row.session_id, createdAt: row.created_at, ...result }];
1512
+ });
1513
+ return c.json({ results: normalized.slice(0, limit) });
1514
+ }
1515
+ catch (err) {
1516
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
1517
+ }
1518
+ });
1519
+ function readableHistorySearchResult(type, rawPayload, query) {
1520
+ let payload;
1521
+ try {
1522
+ payload = JSON.parse(rawPayload);
1523
+ }
1524
+ catch {
1525
+ return null;
1526
+ }
1527
+ let text = null;
1528
+ let kind = 'agent';
1529
+ if (type === 'user:message') {
1530
+ text = typeof payload.content === 'string' ? payload.content : null;
1531
+ kind = 'user';
1532
+ }
1533
+ else if (type === 'agent:event') {
1534
+ if (payload.kind === 'message:text' && typeof payload.text === 'string')
1535
+ text = payload.text;
1536
+ }
1537
+ else if (type === 'agent:output') {
1538
+ const message = payload.message;
1539
+ if (Array.isArray(message?.content)) {
1540
+ text = message.content
1541
+ .flatMap((block) => {
1542
+ if (!block || typeof block !== 'object')
1543
+ return [];
1544
+ const candidate = block;
1545
+ return candidate.type === 'text' && typeof candidate.text === 'string' ? [candidate.text] : [];
1546
+ })
1547
+ .join('\n');
1548
+ }
1549
+ }
1550
+ if (!text)
1551
+ return null;
1552
+ const matchIndex = text.toLowerCase().indexOf(query.toLowerCase());
1553
+ if (matchIndex < 0)
1554
+ return null;
1555
+ const start = Math.max(0, matchIndex - 90);
1556
+ const end = Math.min(text.length, matchIndex + query.length + 180);
1557
+ return {
1558
+ kind,
1559
+ snippet: `${start > 0 ? '…' : ''}${text.slice(start, end).replace(/\s+/g, ' ').trim()}${end < text.length ? '…' : ''}`,
1560
+ };
1561
+ }
1562
+ // GET /api/workspaces/:id/diagnostic.json — portable, redacted-by-design event diagnostic.
1563
+ app.get('/:id/diagnostic.json', (c) => {
1564
+ try {
1565
+ const id = c.req.param('id');
1566
+ const workspace = workspaceService.getWorkspace(id);
1567
+ if (!workspace)
1568
+ return c.json({ error: `Workspace '${id}' not found` }, 404);
1569
+ const db = getDb();
1570
+ const sessions = db
1571
+ .prepare('SELECT id, engine_session_id, status, started_at, ended_at, model, name FROM agent_sessions WHERE workspace_id = ? ORDER BY started_at ASC')
1572
+ .all(id);
1573
+ const events = db
1574
+ .prepare('SELECT id, session_id, type, created_at FROM ws_events WHERE workspace_id = ? ORDER BY rowid ASC')
1575
+ .all(id);
1576
+ c.header('Content-Disposition', `attachment; filename="${workspace.name.replace(/[^a-z0-9]+/gi, '-').toLowerCase() || id}-diagnostic.json"`);
1577
+ return c.json({
1578
+ generatedAt: new Date().toISOString(),
1579
+ workspace: { id, name: workspace.name, engine: workspace.engine },
1580
+ sessions,
1581
+ events,
1582
+ });
1583
+ }
1584
+ catch (err) {
1585
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
1586
+ }
1587
+ });
1468
1588
  // GET /api/workspaces/:id/events — paginated event history (must be before GET /:id for route ordering)
1469
1589
  app.get('/:id/events', (c) => {
1470
1590
  try {
@@ -1474,13 +1594,27 @@ app.get('/:id/events', (c) => {
1474
1594
  return c.json({ error: `Workspace '${id}' not found` }, 404);
1475
1595
  }
1476
1596
  const before = c.req.query('before'); // event ID cursor
1597
+ const around = c.req.query('around'); // event ID to include in a focused history window
1477
1598
  // optional: scope to a session view. Session views also include
1478
1599
  // workspace-level rows where session_id IS NULL (legacy/pre-session items).
1479
1600
  const session = c.req.query('session');
1480
1601
  const limit = Math.min(parseInt(c.req.query('limit') ?? '100', 10) || 100, 500);
1481
1602
  const db = getDb();
1482
1603
  let rows;
1483
- if (before) {
1604
+ if (around) {
1605
+ const targetRow = db.prepare('SELECT rowid FROM ws_events WHERE id = ? AND workspace_id = ?').get(around, id);
1606
+ if (!targetRow)
1607
+ return c.json({ events: [], hasMore: false });
1608
+ const halfWindow = Math.floor(limit / 2);
1609
+ rows = session
1610
+ ? db
1611
+ .prepare('SELECT * FROM ws_events WHERE workspace_id = ? AND (session_id = ? OR session_id IS NULL) AND rowid BETWEEN ? AND ? ORDER BY rowid ASC')
1612
+ .all(id, session, targetRow.rowid - halfWindow, targetRow.rowid + halfWindow)
1613
+ : db
1614
+ .prepare('SELECT * FROM ws_events WHERE workspace_id = ? AND rowid BETWEEN ? AND ? ORDER BY rowid ASC')
1615
+ .all(id, targetRow.rowid - halfWindow, targetRow.rowid + halfWindow);
1616
+ }
1617
+ else if (before) {
1484
1618
  // Get the rowid of the cursor event
1485
1619
  const cursorRow = db.prepare('SELECT rowid FROM ws_events WHERE id = ?').get(before);
1486
1620
  if (!cursorRow) {
@@ -1508,7 +1642,7 @@ app.get('/:id/events', (c) => {
1508
1642
  }
1509
1643
  // Reverse to chronological order (we queried DESC for "before" pagination,
1510
1644
  // or for the "session + no cursor" case where we fetched the newest first).
1511
- if (before || session)
1645
+ if (before || (session && !around))
1512
1646
  rows.reverse();
1513
1647
  const events = rows.map((row) => {
1514
1648
  let parsedPayload;
@@ -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':