@vantran-se/openchamber-web 2.1.1 → 2.2.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 (442) hide show
  1. package/README.md +32 -24
  2. package/bin/cli.js +1 -3
  3. package/bin/cli.test.js +40 -16
  4. package/bin/lib/DOCUMENTATION.md +6 -5
  5. package/bin/lib/cli-args.js +6 -1
  6. package/bin/lib/cli-args.test.js +34 -0
  7. package/bin/lib/cli-lifecycle.js +0 -1
  8. package/bin/lib/cli-network.js +8 -2
  9. package/bin/lib/cli-process.js +1 -1
  10. package/bin/lib/cli-settings-accessors.test.js +21 -13
  11. package/bin/lib/cli-startup.js +17 -11
  12. package/bin/lib/cli-startup.test.js +92 -2
  13. package/bin/lib/commands-connect-url.js +8 -1
  14. package/bin/lib/commands-schedule.js +8 -2
  15. package/bin/lib/commands-serve.js +15 -21
  16. package/bin/lib/commands-update.js +12 -24
  17. package/bin/lib/commands-update.test.js +14 -46
  18. package/dist/assets/DiffView-Cv9MSXl1.js +4 -0
  19. package/dist/assets/FilePreviewCommentMenu-D4kLseHj.js +4 -0
  20. package/dist/assets/FilesView-BVoD8I2x.js +14 -0
  21. package/dist/assets/{GitView-DzuzhIPx.js → GitView-Bzb8p-ew.js} +4 -4
  22. package/dist/assets/GuestSurfacePane-Crdbvsxo.js +1 -0
  23. package/dist/assets/InProgressOperationBanner-CwMjNQgk.js +2 -0
  24. package/dist/assets/{JsonTreeView-B_EPRjzy.js → JsonTreeView-DRKHZfsn.js} +1 -1
  25. package/dist/assets/MarkdownImageGallery-DZtqT1Zn.js +1 -0
  26. package/dist/assets/MarkdownRendererImpl-CqG7HCNd.js +10 -0
  27. package/dist/assets/OnboardingScreen-DMIxwYK5.js +1 -0
  28. package/dist/assets/{PatchDiff-6yh1csza.js → PatchDiff-CUcCg1jk.js} +1 -1
  29. package/dist/assets/PierreDiffViewer-CnnR-dnD.js +228 -0
  30. package/dist/assets/PlanView-RCBadcY7.js +5 -0
  31. package/dist/assets/SettingsView-CjBjZFRO.js +30 -0
  32. package/dist/assets/SettingsWindow-DyM7QuhI.js +1 -0
  33. package/dist/assets/SubmoduleDiffSummary-BWIfgMED.js +1 -0
  34. package/dist/assets/ToolOutputDialog-Q0P3FuAi.js +15 -0
  35. package/dist/assets/ToolPartDiffPreview-p7qR9FpU.js +8 -0
  36. package/dist/assets/{VirtualizedFileDiff-BTJVRkyo.js → VirtualizedFileDiff-CFpjL8Ns.js} +3 -3
  37. package/dist/assets/{Virtualizer-xCEYz34p.js → Virtualizer-C0IsclZn.js} +1 -1
  38. package/dist/assets/WalkthroughView-DI31zD02.js +4 -0
  39. package/dist/assets/appThemeRegistry-B9gkFq9i.js +1 -0
  40. package/dist/assets/{computeVirtualFileMetrics-CmPe29DM.js → computeVirtualFileMetrics-C2WM6JC2.js} +2 -2
  41. package/dist/assets/de-YKfk5MmC.js +14 -0
  42. package/dist/assets/es-_gwT0kr-.js +20 -0
  43. package/dist/assets/fileTreeStatus-D438yi8M.js +1 -0
  44. package/dist/assets/fr-DqzvXmIm.js +18 -0
  45. package/dist/assets/index-BLaHKdtx.js +1 -0
  46. package/dist/assets/index-BkMXRrVQ.css +1 -0
  47. package/dist/assets/index-DxNa7xYB.js +65 -0
  48. package/dist/assets/initializeSharedPreferences-Bck5EAWP.js +1 -0
  49. package/dist/assets/{isDiffPlainText-CQmDCnEv.js → isDiffPlainText-Dgbw8VKk.js} +1 -1
  50. package/dist/assets/{isFilePlainText-CYBUzW4I.js → isFilePlainText-_ZS5UYRj.js} +1 -1
  51. package/dist/assets/ja-B-GFYhQE.js +20 -0
  52. package/dist/assets/ko-mzksfqDC.js +20 -0
  53. package/dist/assets/main-C5Xx8dn_.js +2 -0
  54. package/dist/assets/main-CfBSQM1H.js +990 -0
  55. package/dist/assets/markdownCore-BmWwW1ll.js +7 -0
  56. package/dist/assets/miniChat-mnKwy4vQ.js +2 -0
  57. package/dist/assets/mobile-DeEQd0wZ.js +2 -0
  58. package/dist/assets/nl-CWQr6NSq.js +20 -0
  59. package/dist/assets/{parseDiffFromFile-Cy0PNxun.js → parseDiffFromFile-BUgn-T9p.js} +2 -2
  60. package/dist/assets/patchFileDiff-Dt8LhgBh.js +13 -0
  61. package/dist/assets/pl-UmRcDUX2.js +20 -0
  62. package/dist/assets/pt-BR-BSLybvVF.js +20 -0
  63. package/dist/assets/relocateRestoredSession-B5E9xb3T.js +1 -0
  64. package/dist/assets/renderElectronMiniChatApp-C0a9A5_J.js +2 -0
  65. package/dist/assets/renderMobileApp-BNncg3Pk.js +25 -0
  66. package/dist/assets/runtimeEndpointReset-CZ5aafpl.js +62 -0
  67. package/dist/assets/{selectionMarkdown-TkYk5h9l.js → selectionMarkdown-CUN3wUld.js} +9 -9
  68. package/dist/assets/{sessionOwnership-Bs1xbRfB.js → sessionOwnership-BSuxCEhb.js} +1 -1
  69. package/dist/assets/shikiHighlight-DHDx-6eM.js +1 -0
  70. package/dist/assets/space-apply-DM_2pRW7.js +1 -0
  71. package/dist/assets/space-close-Bpomt8z0.js +1 -0
  72. package/dist/assets/{surface-B6n7wJtt.js → surface-C-jQaGnL.js} +1 -1
  73. package/dist/assets/{textMateThemeFromAppTheme-CDlKffWz.js → textMateThemeFromAppTheme-DlsTphbY.js} +1 -1
  74. package/dist/assets/tr-gaTrX_HK.js +11 -0
  75. package/dist/assets/uk-CnJF6ykl.js +20 -0
  76. package/dist/assets/{useAppFontEffects-DCqCVGlT.js → useAppFontEffects-DGlvOqP_.js} +194 -233
  77. package/dist/assets/useInlineCommentController-kMN4Wg_D.js +2 -0
  78. package/dist/assets/{useRootScrollLock-BowwS8kq.js → useRootScrollLock-DMPpvpNP.js} +2 -2
  79. package/dist/assets/{vendor-base-ui-DX96QhTP.js → vendor-base-ui-Det7U81h.js} +1 -1
  80. package/dist/assets/{vendor-codemirror-autocomplete-DW1FQz9_.js → vendor-codemirror-autocomplete-BSHQHbKu.js} +1 -1
  81. package/dist/assets/{vendor-codemirror-commands-DpmhalwN.js → vendor-codemirror-commands-C4G_dPWe.js} +1 -1
  82. package/dist/assets/{vendor-codemirror-lang-angular-BqExbN1M.js → vendor-codemirror-lang-angular-B6LbBGHB.js} +1 -1
  83. package/dist/assets/{vendor-codemirror-lang-cpp-DZA36HfS.js → vendor-codemirror-lang-cpp-BWWXBx7W.js} +1 -1
  84. package/dist/assets/{vendor-codemirror-lang-css-B07blnnZ.js → vendor-codemirror-lang-css-Cayfb29q.js} +1 -1
  85. package/dist/assets/{vendor-codemirror-lang-go-BM8K9D1-.js → vendor-codemirror-lang-go-BoKlCNKw.js} +1 -1
  86. package/dist/assets/{vendor-codemirror-lang-html-DaH9adfE.js → vendor-codemirror-lang-html-BQWerHsJ.js} +1 -1
  87. package/dist/assets/{vendor-codemirror-lang-java-CsdC0viz.js → vendor-codemirror-lang-java-DWX7Fq_K.js} +1 -1
  88. package/dist/assets/{vendor-codemirror-lang-javascript-CpGYXGuo.js → vendor-codemirror-lang-javascript-DbpEcZM0.js} +2 -2
  89. package/dist/assets/{vendor-codemirror-lang-jinja-BBeGzXw5.js → vendor-codemirror-lang-jinja-D1JIJRA7.js} +1 -1
  90. package/dist/assets/{vendor-codemirror-lang-json-58T70Hqs.js → vendor-codemirror-lang-json-BzgckzmB.js} +1 -1
  91. package/dist/assets/{vendor-codemirror-lang-less-BI-xCA59.js → vendor-codemirror-lang-less-B9EpDXFi.js} +1 -1
  92. package/dist/assets/{vendor-codemirror-lang-liquid-zEcMa-zy.js → vendor-codemirror-lang-liquid-9U7kRbSf.js} +1 -1
  93. package/dist/assets/{vendor-codemirror-lang-markdown-BLKnlLP4.js → vendor-codemirror-lang-markdown-D2TEBIvZ.js} +1 -1
  94. package/dist/assets/{vendor-codemirror-lang-php-9f35M83-.js → vendor-codemirror-lang-php-BT6sDVzE.js} +1 -1
  95. package/dist/assets/vendor-codemirror-lang-python-jAMtYiA9.js +3 -0
  96. package/dist/assets/{vendor-codemirror-lang-rust-MNDL_jZQ.js → vendor-codemirror-lang-rust-CVO56EiL.js} +1 -1
  97. package/dist/assets/{vendor-codemirror-lang-sass-p9epsSNm.js → vendor-codemirror-lang-sass-DZlOoUWu.js} +1 -1
  98. package/dist/assets/{vendor-codemirror-lang-sql-Bqzh0EXX.js → vendor-codemirror-lang-sql-fZ6t1DPw.js} +1 -1
  99. package/dist/assets/{vendor-codemirror-lang-vue-BnNUWV-p.js → vendor-codemirror-lang-vue-DxbqrLiE.js} +1 -1
  100. package/dist/assets/{vendor-codemirror-lang-wast-C2a-oLmk.js → vendor-codemirror-lang-wast-DyU1Gt-I.js} +1 -1
  101. package/dist/assets/vendor-codemirror-lang-xml-C9a_K5_p.js +1 -0
  102. package/dist/assets/{vendor-codemirror-lang-yaml-C0sjswB0.js → vendor-codemirror-lang-yaml-Cptq8l0e.js} +1 -1
  103. package/dist/assets/{vendor-codemirror-language-ByBn5zIW.js → vendor-codemirror-language-DufXoQvE.js} +2 -2
  104. package/dist/assets/vendor-codemirror-language-data-CtwSWKtX.js +2 -0
  105. package/dist/assets/{vendor-codemirror-search-ib-sHD5W.js → vendor-codemirror-search-CvXM_pZg.js} +1 -1
  106. package/dist/assets/{vendor-codemirror-state-G3UuR-be.js → vendor-codemirror-state-Dek9sMtw.js} +1 -1
  107. package/dist/assets/{vendor-codemirror-view-Bniqp7p5.js → vendor-codemirror-view-1e0FQ6Wn.js} +1 -1
  108. package/dist/assets/vendor-framer-motion-DxDVhOLc.js +9 -0
  109. package/dist/assets/vendor-katex-CuQvj9-L.js +257 -0
  110. package/dist/assets/vendor-katex-DdZNUVNG.css +1 -0
  111. package/dist/assets/{vendor-motion-dom-DcCgeG69.js → vendor-motion-dom-HXPOMxb7.js} +1 -1
  112. package/dist/assets/{vendor-opencode-client-DHzhmLwV.js → vendor-opencode-client-BmFcUkbQ.js} +1 -1
  113. package/dist/assets/{vendor-replit-codemirror-vim-DkzqxN2H.js → vendor-replit-codemirror-vim-BlJRn-cW.js} +2 -2
  114. package/dist/assets/{vendor-tanstack-react-virtual-H_JmXOFV.js → vendor-tanstack-react-virtual-CvuLsk7D.js} +1 -1
  115. package/dist/assets/{vendor-tanstack-virtual-core-Dxc8hSTB.js → vendor-tanstack-virtual-core-DPaNcJcT.js} +1 -1
  116. package/dist/assets/zh-CN-t2vigSB1.js +20 -0
  117. package/dist/assets/zh-TW-jAgqBokB.js +20 -0
  118. package/dist/index.html +6 -6
  119. package/dist/mini-chat.html +6 -6
  120. package/dist/mobile.html +6 -6
  121. package/package.json +8 -5
  122. package/server/index.js +80 -36
  123. package/server/lib/agent-memory/actions.js +1 -6
  124. package/server/lib/agent-memory/routes.js +1 -2
  125. package/server/lib/agent-memory/runtime.js +1 -8
  126. package/server/lib/agent-tool/DOCUMENTATION.md +7 -3
  127. package/server/lib/agent-tool/runtime.js +3 -6
  128. package/server/lib/agent-tool/runtime.test.js +0 -2
  129. package/server/lib/browser-control/DOCUMENTATION.md +6 -0
  130. package/server/lib/changelog/update-notes.js +1 -1
  131. package/server/lib/context-obligatory/runtime.js +1 -2
  132. package/server/lib/dev-tunnel/DOCUMENTATION.md +8 -0
  133. package/server/lib/dev-tunnel/client.js +21 -10
  134. package/server/lib/dev-tunnel/tunnel.test.js +34 -0
  135. package/server/lib/dispatch-results/runtime.js +1 -1
  136. package/server/lib/enterprise-mode.d.ts +1 -0
  137. package/server/lib/enterprise-mode.js +13 -2
  138. package/server/lib/enterprise-mode.test.js +21 -0
  139. package/server/lib/event-stream/DOCUMENTATION.md +2 -2
  140. package/server/lib/event-stream/index.js +1 -3
  141. package/server/lib/event-stream/translate-v2.js +1 -9
  142. package/server/lib/event-stream/translate-v2.test.js +0 -11
  143. package/server/lib/fs/DOCUMENTATION.md +5 -2
  144. package/server/lib/fs/routes.js +62 -15
  145. package/server/lib/fs/routes.test.js +278 -48
  146. package/server/lib/fs/workspace-file-names.js +104 -0
  147. package/server/lib/fs/workspace-file-names.test.js +62 -0
  148. package/server/lib/git/DOCUMENTATION.md +17 -5
  149. package/server/lib/git/contributor-provenance-storage.js +9 -6
  150. package/server/lib/git/contributor-provenance-storage.test.js +29 -0
  151. package/server/lib/git/credential-broker.js +1 -1
  152. package/server/lib/git/credential-resolver.js +1 -4
  153. package/server/lib/git/credentials.js +1 -0
  154. package/server/lib/git/discovery-endpoint.js +1 -1
  155. package/server/lib/git/identity-provisioning.js +2 -1
  156. package/server/lib/git/identity-storage.js +3 -4
  157. package/server/lib/git/lfs-discovery.js +14 -14
  158. package/server/lib/git/lfs-discovery.test.js +16 -6
  159. package/server/lib/git/network-operation-plan.js +50 -62
  160. package/server/lib/git/network-operation-plan.test.js +103 -1
  161. package/server/lib/git/network-operation-registry.js +4 -6
  162. package/server/lib/git/network-operation-storage.js +4 -6
  163. package/server/lib/git/network-operations.js +12 -4
  164. package/server/lib/git/network-operations.test.js +19 -6
  165. package/server/lib/git/redaction.js +2 -1
  166. package/server/lib/git/repository-credential-runtime.js +5 -3
  167. package/server/lib/git/repository-credential-runtime.test.js +22 -0
  168. package/server/lib/git/routes.js +31 -137
  169. package/server/lib/git/routes.test.js +2 -2
  170. package/server/lib/git/service.js +143 -27
  171. package/server/lib/git/service.test.js +215 -1
  172. package/server/lib/git/ssh-credential-storage.js +1 -4
  173. package/server/lib/git/submodule-discovery.js +1 -1
  174. package/server/lib/git/worktree-bootstrap-storage.js +3 -4
  175. package/server/lib/git/worktree-bootstrap-storage.test.js +15 -0
  176. package/server/lib/git/worktree-creation.js +194 -0
  177. package/server/lib/git/worktree-creation.test.js +155 -0
  178. package/server/lib/github/DOCUMENTATION.md +11 -6
  179. package/server/lib/github/auth.js +2 -4
  180. package/server/lib/github/device-flow.js +2 -2
  181. package/server/lib/github/gh-cli-credential.js +1 -0
  182. package/server/lib/github/octokit.js +31 -0
  183. package/server/lib/github/pr-status.js +35 -18
  184. package/server/lib/github/pr-status.test.js +271 -3
  185. package/server/lib/github/reference-search.js +102 -13
  186. package/server/lib/github/reference-search.test.js +48 -7
  187. package/server/lib/github/routes.auth.test.js +18 -0
  188. package/server/lib/github/routes.js +360 -6
  189. package/server/lib/github/routes.mutations.test.js +239 -0
  190. package/server/lib/github/routes.test.js +134 -2
  191. package/server/lib/gitlab/DOCUMENTATION.md +5 -1
  192. package/server/lib/gitlab/auth-storage.test.js +1 -0
  193. package/server/lib/gitlab/mappers.js +30 -1
  194. package/server/lib/gitlab/resources.js +230 -6
  195. package/server/lib/gitlab/resources.test.js +179 -0
  196. package/server/lib/gitlab/routes.js +200 -6
  197. package/server/lib/gitlab/routes.test.js +186 -2
  198. package/server/lib/gitlab/validation.js +1 -5
  199. package/server/lib/guests/DOCUMENTATION.md +1 -1
  200. package/server/lib/guests/oauth.js +2 -2
  201. package/server/lib/guests/service.js +1 -1
  202. package/server/lib/guests/sockets.js +1 -1
  203. package/server/lib/injected-env.d.ts +8 -0
  204. package/server/lib/injected-env.js +50 -0
  205. package/server/lib/injected-env.test.js +56 -0
  206. package/server/lib/linear/DOCUMENTATION.md +5 -6
  207. package/server/lib/linear/issues.js +62 -4
  208. package/server/lib/linear/issues.test.js +68 -0
  209. package/server/lib/linear/oauth.js +5 -5
  210. package/server/lib/linear/parse.js +2 -9
  211. package/server/lib/message-queue/DOCUMENTATION.md +23 -1
  212. package/server/lib/message-queue/runtime.js +85 -12
  213. package/server/lib/message-queue/runtime.test.js +172 -2
  214. package/server/lib/notifications/apns-runtime.js +5 -6
  215. package/server/lib/notifications/apns-runtime.test.js +20 -4
  216. package/server/lib/openchamber-control/DOCUMENTATION.md +19 -1
  217. package/server/lib/openchamber-control/actions.js +2 -1
  218. package/server/lib/openchamber-control/service.js +119 -20
  219. package/server/lib/openchamber-control/service.test.js +166 -0
  220. package/server/lib/openchamber-sessions/archive-store.js +1 -6
  221. package/server/lib/openchamber-sessions/routes.js +47 -12
  222. package/server/lib/openchamber-sessions/routes.test.js +65 -2
  223. package/server/lib/openchamber-sessions/session-metadata-store.js +1 -6
  224. package/server/lib/opencode/DOCUMENTATION.md +22 -8
  225. package/server/lib/opencode/agents.js +62 -0
  226. package/server/lib/opencode/auth-state-runtime.js +3 -1
  227. package/server/lib/opencode/auth.d.ts +1 -1
  228. package/server/lib/opencode/auth.js +8 -0
  229. package/server/lib/opencode/auth.test.js +31 -0
  230. package/server/lib/opencode/config-entity-routes.js +16 -0
  231. package/server/lib/opencode/config-entity-v2.test.js +16 -1
  232. package/server/lib/opencode/config-paths.test.js +2 -2
  233. package/server/lib/opencode/config-v2.js +2 -4
  234. package/server/lib/opencode/feature-routes-runtime.js +89 -49
  235. package/server/lib/opencode/lifecycle.js +41 -21
  236. package/server/lib/opencode/lifecycle.test.js +50 -2
  237. package/server/lib/opencode/model-discovery.js +1 -1
  238. package/server/lib/opencode/model-discovery.test.js +17 -0
  239. package/server/lib/opencode/models-metadata.js +0 -2
  240. package/server/lib/opencode/openchamber-routes.test.js +3 -3
  241. package/server/lib/opencode/plugins.js +6 -9
  242. package/server/lib/opencode/provider-source-route.test.js +101 -3
  243. package/server/lib/opencode/providers-disabled.test.js +59 -0
  244. package/server/lib/opencode/providers.js +34 -0
  245. package/server/lib/opencode/proxy.js +11 -3
  246. package/server/lib/opencode/routes.js +40 -2
  247. package/server/lib/opencode/settings-helpers.js +27 -1
  248. package/server/lib/opencode/settings-helpers.test.js +19 -7
  249. package/server/lib/opencode/settings-registry.json +27 -16
  250. package/server/lib/opencode/settings-runtime.test.js +18 -0
  251. package/server/lib/opencode/shared-service-runtime.js +23 -104
  252. package/server/lib/opencode/shared.js +77 -46
  253. package/server/lib/opencode/shared.test.js +12 -12
  254. package/server/lib/opencode/shutdown-runtime.test.js +2 -3
  255. package/server/lib/opencode/skill-routes.js +123 -38
  256. package/server/lib/opencode/skill-routes.test.js +201 -0
  257. package/server/lib/opencode/skills.js +81 -34
  258. package/server/lib/opencode/skills.test.js +176 -21
  259. package/server/lib/opencode/startup-pipeline-runtime.js +6 -3
  260. package/server/lib/opencode/startup-pipeline-runtime.test.js +2 -1
  261. package/server/lib/opencode/watcher.test.js +0 -54
  262. package/server/lib/outbound-proxy.js +52 -0
  263. package/server/lib/outbound-proxy.test.js +55 -0
  264. package/server/lib/package-manager.js +23 -17
  265. package/server/lib/package-manager.test.js +5 -18
  266. package/server/lib/permission-auto-accept/DOCUMENTATION.md +1 -1
  267. package/server/lib/permission-auto-accept/runtime.js +19 -47
  268. package/server/lib/permission-auto-accept/runtime.test.js +2 -2
  269. package/server/lib/project-context/DOCUMENTATION.md +7 -1
  270. package/server/lib/project-context/routes.js +1 -2
  271. package/server/lib/project-context/runtime.js +12 -14
  272. package/server/lib/project-context/runtime.test.js +78 -2
  273. package/server/lib/projects/project-config.js +30 -15
  274. package/server/lib/projects/project-config.test.js +30 -0
  275. package/server/lib/projects/project-setup.js +1 -2
  276. package/server/lib/projects/routes.js +1 -2
  277. package/server/lib/quota/DOCUMENTATION.md +35 -7
  278. package/server/lib/quota/index.js +1 -0
  279. package/server/lib/quota/providers/claude/index.js +1 -1
  280. package/server/lib/quota/providers/cline-pass.js +1 -1
  281. package/server/lib/quota/providers/exe-dev.js +1 -1
  282. package/server/lib/quota/providers/google/index.js +1 -1
  283. package/server/lib/quota/providers/hyper.js +1 -1
  284. package/server/lib/quota/providers/kimi.js +116 -36
  285. package/server/lib/quota/providers/kimi.test.js +128 -3
  286. package/server/lib/quota/providers/opencode-go.js +144 -21
  287. package/server/lib/quota/providers/opencode-go.test.js +229 -6
  288. package/server/lib/quota/providers/openrouter.js +1 -1
  289. package/server/lib/quota/providers/zenmux.js +1 -1
  290. package/server/lib/quota/utils/auth.js +1 -0
  291. package/server/lib/quota/utils/transformers.js +2 -6
  292. package/server/lib/relay/DOCUMENTATION.md +18 -3
  293. package/server/lib/relay/e2ee.js +7 -7
  294. package/server/lib/relay/host-client.js +84 -13
  295. package/server/lib/relay/host-client.test.js +201 -1
  296. package/server/lib/relay/identity.js +6 -29
  297. package/server/lib/relay/identity.test.js +18 -28
  298. package/server/lib/relay/key-store.js +141 -0
  299. package/server/lib/relay/key-store.test.js +138 -0
  300. package/server/lib/relay/service.js +8 -8
  301. package/server/lib/relay/service.test.js +10 -5
  302. package/server/lib/relay/signing-key.js +13 -36
  303. package/server/lib/relay/tunnel-codec.js +10 -10
  304. package/server/lib/route-module-exports.test.js +22 -0
  305. package/server/lib/routing/DOCUMENTATION.md +23 -3
  306. package/server/lib/routing/cache.js +87 -0
  307. package/server/lib/routing/defaults.js +11 -0
  308. package/server/lib/routing/history.js +12 -4
  309. package/server/lib/routing/runtime.js +39 -3
  310. package/server/lib/routing/runtime.test.js +81 -2
  311. package/server/lib/routing/store.js +12 -1
  312. package/server/lib/routing/store.test.js +13 -0
  313. package/server/lib/scheduled-tasks/DOCUMENTATION.md +59 -1
  314. package/server/lib/scheduled-tasks/existing-session.js +119 -0
  315. package/server/lib/scheduled-tasks/existing-session.test.js +350 -0
  316. package/server/lib/scheduled-tasks/loops.js +1 -8
  317. package/server/lib/scheduled-tasks/routes.js +8 -8
  318. package/server/lib/scheduled-tasks/runtime.js +103 -13
  319. package/server/lib/scheduled-tasks/runtime.test.js +48 -0
  320. package/server/lib/scheduled-tasks/service.js +33 -11
  321. package/server/lib/scheduled-tasks/service.test.js +128 -2
  322. package/server/lib/scheduled-tasks/session-defaults.js +27 -0
  323. package/server/lib/scheduled-tasks/session-defaults.test.js +19 -0
  324. package/server/lib/session-assist/context.js +1 -0
  325. package/server/lib/session-folders/routes.js +2 -2
  326. package/server/lib/session-goal/objectives.js +1 -1
  327. package/server/lib/session-knowledge/routes.js +2 -2
  328. package/server/lib/session-knowledge/runtime.js +1 -2
  329. package/server/lib/session-work/DOCUMENTATION.md +39 -14
  330. package/server/lib/session-work/questions.js +31 -2
  331. package/server/lib/session-work/runtime.js +22 -13
  332. package/server/lib/session-work/runtime.test.js +43 -26
  333. package/server/lib/session-work/state.js +10 -4
  334. package/server/lib/session-work/state.test.js +8 -1
  335. package/server/lib/shared/DOCUMENTATION.md +89 -0
  336. package/server/lib/shared/guards.js +53 -0
  337. package/server/lib/shared/guards.test.js +112 -0
  338. package/server/lib/skills-catalog/DOCUMENTATION.md +10 -8
  339. package/server/lib/skills-catalog/cache.js +3 -2
  340. package/server/lib/skills-catalog/curated-sources.js +5 -1
  341. package/server/lib/skills-catalog/git.js +107 -20
  342. package/server/lib/skills-catalog/git.test.js +78 -0
  343. package/server/lib/skills-catalog/install.js +21 -12
  344. package/server/lib/skills-catalog/scan.js +25 -15
  345. package/server/lib/skills-catalog/source.js +26 -1
  346. package/server/lib/skills-catalog/source.test.js +27 -0
  347. package/server/lib/source-control/DOCUMENTATION.md +2 -1
  348. package/server/lib/source-control/audit-storage.js +7 -6
  349. package/server/lib/source-control/audit-storage.test.js +11 -0
  350. package/server/lib/source-control/binding-service.js +2 -5
  351. package/server/lib/source-control/change-request-worktree.js +90 -0
  352. package/server/lib/source-control/change-request-worktree.test.js +121 -0
  353. package/server/lib/source-control/mutation-executor.js +1 -1
  354. package/server/lib/source-control/mutation-storage.js +11 -5
  355. package/server/lib/source-control/provider-instance.js +1 -2
  356. package/server/lib/source-control/repository-identity.js +1 -1
  357. package/server/lib/source-control/routes.js +14 -2
  358. package/server/lib/spaces/DOCUMENTATION.md +42 -17
  359. package/server/lib/spaces/command-stream.test.js +15 -0
  360. package/server/lib/spaces/dispatcher.js +7 -0
  361. package/server/lib/spaces/dispatcher.test.js +17 -0
  362. package/server/lib/spaces/hardening.js +2 -2
  363. package/server/lib/spaces/hardening.test.js +1 -1
  364. package/server/lib/spaces/host.js +2 -0
  365. package/server/lib/spaces/journey.js +66 -5
  366. package/server/lib/spaces/journey.test.js +95 -4
  367. package/server/lib/spaces/layout.js +1 -1
  368. package/server/lib/spaces/places/docker-disk.js +38 -14
  369. package/server/lib/spaces/places/docker-disk.test.js +80 -1
  370. package/server/lib/spaces/places/docker.js +35 -11
  371. package/server/lib/spaces/places/fake-docker.js +22 -3
  372. package/server/lib/spaces/places/memory-place.js +3 -0
  373. package/server/lib/spaces/places/packed.docker.live.test.js +1 -1
  374. package/server/lib/spaces/routes.js +5 -0
  375. package/server/lib/spaces/routes.test.js +4 -2
  376. package/server/lib/spaces/run-command.js +9 -2
  377. package/server/lib/spaces/tools.js +4 -5
  378. package/server/lib/spaces/tools.test.js +2 -2
  379. package/server/lib/terminal/runtime.js +18 -6
  380. package/server/lib/terminal/runtime.test.js +66 -0
  381. package/server/lib/tts/index.js +3 -0
  382. package/server/lib/tts/service.js +4 -2
  383. package/server/lib/walkthrough/DOCUMENTATION.md +3 -1
  384. package/server/lib/walkthrough/index.js +2 -3
  385. package/server/lib/walkthrough/pr-source.test.js +22 -0
  386. package/server/lib/walkthrough/sources.js +6 -1
  387. package/server/opencode-proxy.test.js +127 -0
  388. package/bin/lib/commands-startup.test.js +0 -15
  389. package/dist/assets/CodeMirrorCommentWidgets-CVDSjcrP.js +0 -1
  390. package/dist/assets/DiffView-Ct7vL3fD.js +0 -4
  391. package/dist/assets/FilesView-DT0uEYgT.js +0 -17
  392. package/dist/assets/GuestSurfacePane-DVvk1VPu.js +0 -1
  393. package/dist/assets/LinearIssuesView-C10MwA9h.js +0 -1
  394. package/dist/assets/MarkdownImageGallery-BbN_K_3d.js +0 -1
  395. package/dist/assets/MarkdownRendererImpl-BLt2Tkk9.js +0 -10
  396. package/dist/assets/OnboardingScreen-Bgp5A3WB.js +0 -1
  397. package/dist/assets/PierreDiffViewer-CWJLj-S-.js +0 -210
  398. package/dist/assets/PlanView-DsRWnsOI.js +0 -5
  399. package/dist/assets/PublishDialog-CCeKloRu.js +0 -2
  400. package/dist/assets/SettingsView-BuPPsNPp.js +0 -30
  401. package/dist/assets/SettingsWindow-CM5fL8Ot.js +0 -1
  402. package/dist/assets/SubmoduleDiffSummary-CfsMcfiQ.js +0 -1
  403. package/dist/assets/ToolOutputDialog-mZrfSofz.js +0 -15
  404. package/dist/assets/ToolPartDiffPreview-BUGCRYEL.js +0 -8
  405. package/dist/assets/WalkthroughView-ClBo7PPd.js +0 -4
  406. package/dist/assets/appThemeRegistry-CxzMxR-H.js +0 -1
  407. package/dist/assets/de-CBla4hjd.js +0 -14
  408. package/dist/assets/es-BmnZtxTD.js +0 -20
  409. package/dist/assets/fileTreeStatus-BSHFIGEg.js +0 -1
  410. package/dist/assets/fr-OVKIBUZO.js +0 -18
  411. package/dist/assets/index-CKCFKmjF.js +0 -1
  412. package/dist/assets/index-Dbx4V4GN.css +0 -1
  413. package/dist/assets/index-vQ1Pg1cr.js +0 -61
  414. package/dist/assets/ja-6v-yr-gE.js +0 -20
  415. package/dist/assets/ko-I_ZLvgQ9.js +0 -20
  416. package/dist/assets/main-CmO4dRtR.js +0 -2
  417. package/dist/assets/main-b9vqhlYM.js +0 -999
  418. package/dist/assets/markdownCore-BPEZRQfP.js +0 -7
  419. package/dist/assets/miniChat-B5Oi7B-6.js +0 -2
  420. package/dist/assets/mobile-CVtShif9.js +0 -2
  421. package/dist/assets/nl-DcvIJf46.js +0 -20
  422. package/dist/assets/patchFileDiff-CW3wCH1O.js +0 -13
  423. package/dist/assets/pl-JxCUl0sd.js +0 -20
  424. package/dist/assets/pt-BR-L89mjja2.js +0 -20
  425. package/dist/assets/relocateRestoredSession-DYh_9oBa.js +0 -1
  426. package/dist/assets/renderElectronMiniChatApp-BD-1JHSg.js +0 -2
  427. package/dist/assets/renderMobileApp-7s5InXt1.js +0 -25
  428. package/dist/assets/runtimeEndpointReset-DqLJC6L1.js +0 -53
  429. package/dist/assets/shikiHighlight-CXgZnvHh.js +0 -1
  430. package/dist/assets/space-apply-1trGVIl8.js +0 -1
  431. package/dist/assets/space-close-y7j-NuBv.js +0 -1
  432. package/dist/assets/tr-ka5J1flt.js +0 -11
  433. package/dist/assets/uk-PyGe6Q_u.js +0 -20
  434. package/dist/assets/useInlineCommentController-CrjSGCpi.js +0 -2
  435. package/dist/assets/vendor-codemirror-lang-python-B7ClrMIT.js +0 -3
  436. package/dist/assets/vendor-codemirror-lang-xml-C8tGnS64.js +0 -1
  437. package/dist/assets/vendor-codemirror-language-data-q2bKO1EM.js +0 -2
  438. package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +0 -1
  439. package/dist/assets/vendor-katex-CHD9BdDO.js +0 -257
  440. package/dist/assets/vendor-katex-Cp579UoJ.css +0 -1
  441. package/dist/assets/zh-CN-B0P2tybJ.js +0 -20
  442. package/dist/assets/zh-TW-Bs9C5cN8.js +0 -20
@@ -350,6 +350,54 @@ describe('scheduled-tasks runtime prompt dispatch', () => {
350
350
  expect(dispatch[2].body).toMatchObject({ text: 'Review open issues' });
351
351
  expect(dispatch[2].body.resume).toBeUndefined();
352
352
  });
353
+
354
+ it('points a run that fails after creating its session at that session', async () => {
355
+ vi.stubGlobal('fetch', vi.fn(async (input, init = {}) => {
356
+ const { pathname } = new URL(String(input));
357
+ if (init.method === 'POST' && pathname.endsWith('/prompt')) {
358
+ return new Response(JSON.stringify({ error: 'provider down' }), { status: 500 });
359
+ }
360
+ const data = pathname === '/api/session' ? { id: 'ses_failed' } : pathname === '/api/command' ? [] : {};
361
+ return new Response(JSON.stringify({ location: { directory: '/repo' }, data }), { status: 200, headers: { 'content-type': 'application/json' } });
362
+ }));
363
+ const task = {
364
+ id: 'task-1',
365
+ name: 'Nightly',
366
+ enabled: true,
367
+ schedule: { kind: 'daily', times: ['03:00'], timezone: 'UTC' },
368
+ execution: { prompt: 'Review open issues', providerID: 'openai', modelID: 'gpt-5' },
369
+ state: { createdAt: 1, updatedAt: 1, lastSessionId: 'ses_previous' },
370
+ };
371
+ const patches = [];
372
+ const runtime = createScheduledTasksRuntime({
373
+ projectConfigRuntime: {
374
+ listScheduledTasks: async () => [task],
375
+ reconcileLoopTasks: async () => [task],
376
+ updateScheduledTaskState: async (_projectID, _taskID, patch) => {
377
+ patches.push(patch);
378
+ return { task, updated: true };
379
+ },
380
+ updateScheduledTaskStateIf: async () => ({ task, updated: true }),
381
+ },
382
+ listProjects: async () => [{ id: 'proj', path: '/repo' }],
383
+ buildOpenCodeUrl: () => 'http://127.0.0.1:1/',
384
+ getOpenCodeAuthHeaders: () => ({}),
385
+ waitForOpenCodeReady: async () => {},
386
+ logger: { info: () => {}, warn: () => {}, error: () => {} },
387
+ });
388
+ await runtime.start();
389
+ const result = await runtime.runNow('proj', 'task-1');
390
+ runtime.stop();
391
+
392
+ expect(result.ok).toBe(false);
393
+ // The start clears the previous run's session, creation records this one,
394
+ // and the failure keeps it.
395
+ const startIndex = patches.findIndex((patch) => patch.lastStatus === 'running');
396
+ expect(Object.hasOwn(patches[startIndex], 'lastSessionId')).toBe(true);
397
+ expect(patches[startIndex].lastSessionId).toBeUndefined();
398
+ expect(patches[startIndex + 1]).toEqual({ lastSessionId: 'ses_failed' });
399
+ expect(patches.at(-1)).toMatchObject({ lastStatus: 'error', lastSessionId: 'ses_failed' });
400
+ });
353
401
  });
354
402
 
355
403
  describe('scheduled-tasks runtime chats scope', () => {
@@ -1,13 +1,13 @@
1
+ import { asNonEmptyString } from '../shared/guards.js';
1
2
  import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { OpenChamberControlError } from '../openchamber-control/error.js';
4
- import { loopFingerprint, parseLoopDefinition, setLoopFileEnabled } from './loops.js';
5
+ import { discoverLoopFiles, loopFingerprint, parseLoopDefinition, setLoopFileEnabled } from './loops.js';
5
6
 
6
- const asNonEmptyString = (value) => {
7
- if (typeof value !== 'string') return null;
8
- const trimmed = value.trim();
9
- return trimmed.length > 0 ? trimmed : null;
10
- };
7
+ // Same scope rule the scheduler applies: a file under the project's own
8
+ // `.agents/loops` is a repository loop, even where it is also the user folder.
9
+ const isRepositoryLoop = (projectPath, loopFile) => discoverLoopFiles(projectPath)
10
+ .some((entry) => entry.scope === 'project' && entry.filePath === loopFile);
11
11
 
12
12
  export const createScheduledTaskService = (dependencies) => {
13
13
  const {
@@ -111,6 +111,10 @@ export const createScheduledTaskService = (dependencies) => {
111
111
  throw new OpenChamberControlError('task payload is required', 400);
112
112
  }
113
113
  let upserted;
114
+ const previous = taskInput.id ? (await projectConfigRuntime.listScheduledTasks(id)).find((task) => task.id === taskInput.id) : null;
115
+ if (taskInput.targetSessionId && (taskInput.enabled !== false || previous?.targetSessionId !== taskInput.targetSessionId)) {
116
+ await scheduledTasksRuntime.validateTarget(id, taskInput.targetSessionId);
117
+ }
114
118
  try {
115
119
  upserted = await projectConfigRuntime.upsertScheduledTask(id, taskInput);
116
120
  } catch (error) {
@@ -118,6 +122,9 @@ export const createScheduledTaskService = (dependencies) => {
118
122
  const invalid = message.toLowerCase().includes('required') || message.toLowerCase().includes('invalid');
119
123
  throw new OpenChamberControlError(message, invalid ? 400 : 500);
120
124
  }
125
+ if (previous?.targetSessionId && ((previous.enabled && upserted.task.enabled === false) || previous.targetSessionId !== upserted.task.targetSessionId)) {
126
+ await scheduledTasksRuntime.cancelWaitingPrompt(id, taskInput.id);
127
+ }
121
128
  await scheduledTasksRuntime.syncProject(id);
122
129
  const tasks = await projectConfigRuntime.listScheduledTasks(id);
123
130
  return {
@@ -146,6 +153,7 @@ export const createScheduledTaskService = (dependencies) => {
146
153
  }
147
154
  const result = await projectConfigRuntime.deleteScheduledTask(id, normalizedTaskID);
148
155
  if (!result.deleted) throw new OpenChamberControlError('Task not found', 404);
156
+ await scheduledTasksRuntime.cancelWaitingPrompt?.(id, normalizedTaskID);
149
157
  await scheduledTasksRuntime.syncProject(id);
150
158
  return projectConfigRuntime.listScheduledTasks(id);
151
159
  };
@@ -156,11 +164,11 @@ export const createScheduledTaskService = (dependencies) => {
156
164
  if (!normalizedTaskID) throw new OpenChamberControlError('taskId is required', 400);
157
165
  const result = await scheduledTasksRuntime.runNow(id, normalizedTaskID);
158
166
  if (result.running || result.queued) {
159
- throw new OpenChamberControlError(result.error || 'Task already running', 409);
167
+ throw new OpenChamberControlError(result.error || 'Task already running', 409, { busy: result.queued ? 'queued' : 'running' });
160
168
  }
161
169
  if (result.skipped) throw new OpenChamberControlError('Task not found or disabled', 404);
162
170
  if (!result.ok) {
163
- throw new OpenChamberControlError(result.error || 'Task run failed', 500, { task: result.task });
171
+ throw new OpenChamberControlError(result.error || 'Task run failed', result.statusCode || 500, { task: result.task });
164
172
  }
165
173
  const response = {
166
174
  task: result.task,
@@ -173,12 +181,26 @@ export const createScheduledTaskService = (dependencies) => {
173
181
  return response;
174
182
  };
175
183
 
184
+ // The agent tool and the CLI. A loop task follows its file, so it changes
185
+ // the way the Scheduled tasks checkbox does. A repository loop is the
186
+ // exception for enabling: that is the user's own decision on this machine,
187
+ // made in Scheduled tasks, never by an agent or a script on their behalf.
176
188
  const setEnabled = async (projectID, taskID, enabled) => {
177
- const tasks = await list(projectID);
189
+ const project = await findProjectByID(projectID);
190
+ const tasks = await scheduledTasksRuntime.syncProject(project.id);
178
191
  const task = tasks.find((entry) => entry?.id === taskID);
179
192
  if (!task) throw new OpenChamberControlError('Task not found', 404);
180
- const result = await upsert(projectID, { ...task, enabled });
181
- return result.task;
193
+ if (!task.loopFile) {
194
+ const result = await upsert(project.id, { ...task, enabled });
195
+ return result.task;
196
+ }
197
+ // Nothing to change: the loop file, which may be tracked in the
198
+ // repository, and the approval stay untouched.
199
+ if (enabled === task.enabled) return task;
200
+ if (enabled && isRepositoryLoop(project.path, task.loopFile)) {
201
+ throw new OpenChamberControlError(`"${task.name}" comes from this repository. Enable it in Scheduled tasks.`, 409);
202
+ }
203
+ return setLoopEnabled(project.id, task.id, enabled);
182
204
  };
183
205
 
184
206
  const status = async () => {
@@ -1,10 +1,12 @@
1
- import { describe, expect, it, vi } from 'vitest';
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  import os from 'os';
3
3
  import path from 'path';
4
- import { mkdtemp, readFile, rm, writeFile } from 'fs/promises';
4
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from 'fs/promises';
5
5
  import { createScheduledTaskService } from './service.js';
6
6
  import { registerScheduledTaskRoutes } from './routes.js';
7
7
  import { CHATS_SCOPE_PUBLIC_ID, createChatsScope } from './chats-scope.js';
8
+ import { createScheduledTasksRuntime } from './runtime.js';
9
+ import { createProjectConfigRuntime } from '../projects/project-config.js';
8
10
 
9
11
  const createService = (overrides = {}) => {
10
12
  const projectConfigRuntime = {
@@ -349,3 +351,127 @@ Run the digest.
349
351
  }
350
352
  });
351
353
  });
354
+
355
+ // The agent tool and the CLI enable and disable through setEnabled; these run
356
+ // it against the real project config and scheduler, as the server does.
357
+ describe('scheduled-task service setEnabled on loop tasks', () => {
358
+ const loopFileContent = (name, enabled, prompt = `Run ${name}.`) => `---
359
+ name: ${name}
360
+ schedule: "0 9 * * *"
361
+ enabled: ${enabled}
362
+ model: openai/gpt-5
363
+ ---
364
+ ${prompt}
365
+ `;
366
+
367
+ let tempRoot;
368
+ let repoPath;
369
+ let repoLoop;
370
+ let userLoop;
371
+ let service;
372
+ let projectConfigRuntime;
373
+ const savedHome = { HOME: process.env.HOME, USERPROFILE: process.env.USERPROFILE };
374
+
375
+ beforeEach(async () => {
376
+ tempRoot = await mkdtemp(path.join(os.tmpdir(), 'oc-loop-set-enabled-'));
377
+ // User-scope loops live under the home directory.
378
+ const home = path.join(tempRoot, 'home');
379
+ process.env.HOME = home;
380
+ process.env.USERPROFILE = home;
381
+ repoPath = path.join(tempRoot, 'repo');
382
+ await mkdir(path.join(repoPath, '.agents', 'loops'), { recursive: true });
383
+ await mkdir(path.join(home, '.agents', 'loops'), { recursive: true });
384
+ repoLoop = path.join(repoPath, '.agents', 'loops', 'repo.md');
385
+ userLoop = path.join(home, '.agents', 'loops', 'mine.md');
386
+ await writeFile(repoLoop, loopFileContent('repo-loop', true), 'utf8');
387
+ await writeFile(userLoop, loopFileContent('my-loop', false), 'utf8');
388
+
389
+ projectConfigRuntime = createProjectConfigRuntime({
390
+ fsPromises: await import('fs/promises'),
391
+ path,
392
+ projectsDirPath: path.join(tempRoot, 'config'),
393
+ createTaskID: () => 'task-fixed-id',
394
+ });
395
+ const listProjects = async () => [{ id: 'proj', path: repoPath }];
396
+ const scheduledTasksRuntime = createScheduledTasksRuntime({
397
+ buildOpenCodeUrl: () => 'http://localhost',
398
+ getOpenCodeAuthHeaders: () => ({}),
399
+ waitForOpenCodeReady: async () => {},
400
+ projectConfigRuntime,
401
+ listProjects,
402
+ });
403
+ service = createScheduledTaskService({
404
+ readSettingsFromDiskMigrated: async () => ({ projects: await listProjects() }),
405
+ sanitizeProjects: (projects) => projects,
406
+ projectConfigRuntime,
407
+ scheduledTasksRuntime,
408
+ });
409
+ });
410
+
411
+ afterEach(async () => {
412
+ for (const [key, value] of Object.entries(savedHome)) {
413
+ if (value === undefined) delete process.env[key];
414
+ else process.env[key] = value;
415
+ }
416
+ await rm(tempRoot, { recursive: true, force: true });
417
+ });
418
+
419
+ const stored = async (taskID) => (await projectConfigRuntime.listScheduledTasks('proj')).find((task) => task.id === taskID);
420
+ const listed = async (taskID) => (await service.list('proj')).find((task) => task.id === taskID);
421
+
422
+ it('refuses to enable a repository loop and changes nothing', async () => {
423
+ expect(await listed('loop:project:repo-loop')).toMatchObject({ enabled: false, loopApproval: 'required' });
424
+
425
+ await expect(service.setEnabled('proj', 'loop:project:repo-loop', true)).rejects.toMatchObject({
426
+ statusCode: 409,
427
+ message: '"repo-loop" comes from this repository. Enable it in Scheduled tasks.',
428
+ });
429
+
430
+ expect((await stored('loop:project:repo-loop')).enabled).toBe(false);
431
+ expect(await readFile(repoLoop, 'utf8')).toBe(loopFileContent('repo-loop', true));
432
+ expect(await listed('loop:project:repo-loop')).toMatchObject({ enabled: false, loopApproval: 'required' });
433
+ });
434
+
435
+ it('leaves a held-back repository loop file alone when asked to disable it', async () => {
436
+ await expect(service.setEnabled('proj', 'loop:project:repo-loop', false)).resolves.toMatchObject({ enabled: false });
437
+
438
+ expect(await readFile(repoLoop, 'utf8')).toBe(loopFileContent('repo-loop', true));
439
+ expect(await listed('loop:project:repo-loop')).toMatchObject({ enabled: false, loopApproval: 'required' });
440
+ });
441
+
442
+ it('disables a repository loop the user enabled, and it stays disabled', async () => {
443
+ await service.setLoopEnabled('proj', 'loop:project:repo-loop', true);
444
+ const enabled = await listed('loop:project:repo-loop');
445
+ expect(enabled.enabled).toBe(true);
446
+ expect(enabled.loopApproval).toBeUndefined();
447
+
448
+ await expect(service.setEnabled('proj', 'loop:project:repo-loop', false)).resolves.toMatchObject({ enabled: false });
449
+
450
+ const disabled = await listed('loop:project:repo-loop');
451
+ expect(disabled.enabled).toBe(false);
452
+ // Paused by the user: nothing to explain on the card.
453
+ expect(disabled.loopApproval).toBeUndefined();
454
+ expect(await readFile(repoLoop, 'utf8')).toContain('enabled: false');
455
+ // And the agent cannot turn it back on.
456
+ await expect(service.setEnabled('proj', 'loop:project:repo-loop', true)).rejects.toMatchObject({ statusCode: 409 });
457
+ });
458
+
459
+ it('says a repository loop changed since the user enabled it', async () => {
460
+ await service.setLoopEnabled('proj', 'loop:project:repo-loop', true);
461
+ await writeFile(repoLoop, loopFileContent('repo-loop', true, 'Run something else.'), 'utf8');
462
+
463
+ expect(await listed('loop:project:repo-loop')).toMatchObject({ enabled: false, loopApproval: 'outdated' });
464
+ expect((await stored('loop:project:repo-loop')).loopApproval).toBeUndefined();
465
+ await expect(service.setEnabled('proj', 'loop:project:repo-loop', true)).rejects.toMatchObject({ statusCode: 409 });
466
+ });
467
+
468
+ it('enables and disables a loop from the user folder', async () => {
469
+ await expect(service.setEnabled('proj', 'loop:user:my-loop', true)).resolves.toMatchObject({ enabled: true });
470
+ expect((await stored('loop:user:my-loop')).enabled).toBe(true);
471
+ expect(await readFile(userLoop, 'utf8')).toContain('enabled: true');
472
+
473
+ await expect(service.setEnabled('proj', 'loop:user:my-loop', false)).resolves.toMatchObject({ enabled: false });
474
+ expect((await stored('loop:user:my-loop')).enabled).toBe(false);
475
+ expect(await readFile(userLoop, 'utf8')).toContain('enabled: false');
476
+ });
477
+ });
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The model, thinking level and agent a new session starts with, as Settings
3
+ * and the project define them: the project's own default first, then the
4
+ * global one. A thinking level travels with the model it was set next to.
5
+ * Anything unset is null, and OpenCode then applies its own default.
6
+ */
7
+ const splitModel = (value) => {
8
+ const text = typeof value === 'string' ? value.trim() : '';
9
+ const slash = text.indexOf('/');
10
+ if (slash <= 0 || slash === text.length - 1) return null;
11
+ return { providerID: text.slice(0, slash), modelID: text.slice(slash + 1) };
12
+ };
13
+
14
+ const text = (value) => (typeof value === 'string' && value.trim() ? value.trim() : null);
15
+
16
+ export const resolveSessionDefaults = ({ settings, project }) => {
17
+ const projectModel = splitModel(project?.defaultModel);
18
+ const globalModel = splitModel(settings?.defaultModel);
19
+ const model = projectModel ?? globalModel;
20
+ const variant = projectModel ? text(project?.defaultVariant) : globalModel ? text(settings?.defaultVariant) : null;
21
+ return {
22
+ providerID: model?.providerID ?? null,
23
+ modelID: model?.modelID ?? null,
24
+ variant,
25
+ agent: text(project?.defaultAgent) ?? text(settings?.defaultAgent),
26
+ };
27
+ };
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { resolveSessionDefaults } from './session-defaults.js';
4
+
5
+ describe('resolveSessionDefaults', () => {
6
+ it('prefers the project default, then the global one, keeping each variant with its model', () => {
7
+ const settings = { defaultModel: 'anthropic/claude-sonnet-5', defaultVariant: 'high', defaultAgent: 'build' };
8
+ expect(resolveSessionDefaults({ settings, project: { defaultModel: 'openai/gpt-6', defaultAgent: 'plan' } }))
9
+ .toEqual({ providerID: 'openai', modelID: 'gpt-6', variant: null, agent: 'plan' });
10
+ expect(resolveSessionDefaults({ settings, project: {} }))
11
+ .toEqual({ providerID: 'anthropic', modelID: 'claude-sonnet-5', variant: 'high', agent: 'build' });
12
+ });
13
+
14
+ it('leaves everything to OpenCode when nothing is set', () => {
15
+ expect(resolveSessionDefaults({ settings: {}, project: null }))
16
+ .toEqual({ providerID: null, modelID: null, variant: null, agent: null });
17
+ expect(resolveSessionDefaults({ settings: { defaultModel: 'no-slash' }, project: null }).modelID).toBeNull();
18
+ });
19
+ });
@@ -111,6 +111,7 @@ function readMessage(message) {
111
111
  id: message.id,
112
112
  role,
113
113
  created: Number.isFinite(message.time?.created) ? message.time.created : null,
114
+ completed: Number.isFinite(message.time?.completed) ? message.time.completed : null,
114
115
  transparent: isTransparent(message),
115
116
  // v2 has no `parentID` on a message: a turn is the run of messages between
116
117
  // one user message and the assistant reply that follows it, which is what
@@ -1,6 +1,6 @@
1
- const MAX_BODY_BYTES = 4 * 1024 * 1024;
1
+ import { isRecord as isObjectRecord } from '../shared/guards.js';
2
2
 
3
- const isObjectRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
3
+ const MAX_BODY_BYTES = 4 * 1024 * 1024;
4
4
 
5
5
  const hasValidFolderShape = (folder) => (
6
6
  isObjectRecord(folder)
@@ -24,7 +24,7 @@ const goalsDir = () => path.join(
24
24
 
25
25
  const objectiveFilePath = (sessionId) => path.join(goalsDir(), `${sessionId}.md`);
26
26
 
27
- export const isValidObjectiveKey = (sessionId) =>
27
+ const isValidObjectiveKey = (sessionId) =>
28
28
  typeof sessionId === 'string' && SESSION_ID_PATTERN.test(sessionId);
29
29
 
30
30
  const clampContent = (content) => String(content ?? '').trim().slice(0, GOAL_OBJECTIVE_CHAR_LIMIT);
@@ -13,9 +13,9 @@
13
13
 
14
14
  import express from 'express';
15
15
 
16
- const parseJsonBody = express.json({ limit: '1mb' });
16
+ import { isRecord } from '../shared/guards.js';
17
17
 
18
- const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
18
+ const parseJsonBody = express.json({ limit: '1mb' });
19
19
 
20
20
  const asNonEmptyString = (value) => (
21
21
  typeof value === 'string' && value.trim().length > 0 ? value.trim() : ''
@@ -14,6 +14,7 @@
14
14
  * it survives compaction: the tab goes on believing the agent still has context
15
15
  * that has just been summarised away.
16
16
  */
17
+ import { isRecord } from '../shared/guards.js';
17
18
 
18
19
  const KNOWLEDGE_METADATA_KEY = 'knowledge_context_delivered';
19
20
  const PINS_METADATA_KEY = 'project_context_pins';
@@ -21,8 +22,6 @@ const PINS_METADATA_KEY = 'project_context_pins';
21
22
  /** Total budget for the assembled block; anything past it is cut, loudly. */
22
23
  const KNOWLEDGE_MAX_LENGTH = 8000;
23
24
 
24
- const isRecord = (value) => Boolean(value && typeof value === 'object' && !Array.isArray(value));
25
-
26
25
  const truncate = (value, budget) => (
27
26
  value.length <= budget ? value : `${value.slice(0, Math.max(0, budget - 1))}…`
28
27
  );
@@ -4,8 +4,11 @@
4
4
 
5
5
  Keeps sessions where real work is going on in one block at the top of the
6
6
  sidebar until the user marks them done. Jev moves a session in when work
7
- starts in it; the user alone closes it. The same turn-end Jev call tells
8
- session assist whether the Small Model is worth waking.
7
+ starts in it; the user alone closes it. While a session is in work, Jev also
8
+ says after a turn whether the work looks done. Independently of work, the
9
+ same call says whether the turn handed over changes worth a look (the
10
+ composer then offers an AI review or a walkthrough). The same turn-end Jev call tells session assist whether the
11
+ Small Model is worth waking.
9
12
 
10
13
  ## Files
11
14
 
@@ -25,6 +28,10 @@ session assist whether the Small Model is worth waking.
25
28
  - `doneAt` once the user closed it
26
29
  - `suggestDoneAt` while Jev thinks the last turn closed the work out
27
30
 
31
+ `metadata.openchamber.reviewOffer`: `{ at }` while Jev thinks the last turn
32
+ handed over changes the user could look over. It lives outside `work` so it
33
+ needs neither the In work block nor a session in work.
34
+
28
35
  The UI parser is `packages/ui/src/lib/sessionWorkMetadata.ts`; the user's own
29
36
  Track / Done go through the generic metadata route
30
37
  (`setSessionWorkState` in `packages/ui/src/sync/session-actions.ts`), which
@@ -32,7 +39,6 @@ also exists in VS Code.
32
39
 
33
40
  ## Invariants
34
41
 
35
- - Jev only evaluates sessions adopted by OpenChamber (`metadata.openchamber.adopted === true`). Sessions seen only through the shared local OpenCode service remain TUI-owned; viewing one does not send its content to Jev. Creating a session or sending into it from an OpenChamber surface persists adoption first.
36
42
  - Jev only opens. Nothing here ever writes `done`.
37
43
  - Jev reopens a closed session only for a request sent after the user closed
38
44
  it (`openByJevPatch` compares the request's `time.created` with `doneAt`).
@@ -40,13 +46,14 @@ also exists in VS Code.
40
46
  store's per-session write queue, so a user's Done written a moment earlier
41
47
  is seen and kept.
42
48
  - A turn-end check is bound to its turn: every turn start and new user
43
- message advances a per-session counter, and the done hint is written only if
49
+ message advances a per-session counter, and a hint is written only if
44
50
  the counter is unchanged when the write runs (decided inside the store's
45
51
  queue). A late Jev answer about a turn the user already moved past writes
46
52
  nothing.
47
- - The done hint is current while `suggestDoneAt >= session.time.idle` (the
48
- assist's freshness rule). This runtime also deletes a hint it wrote when the
49
- next turn starts; the rule covers hints written by another process.
53
+ - A turn writes at most one hint: looks done wins over the review offer.
54
+ - A hint is current while its time `>= session.time.idle` (the assist's
55
+ freshness rule). This runtime also deletes a hint it wrote when the next
56
+ turn starts; the rule covers hints written by another process.
50
57
  - Only top-level, unarchived project sessions that are not review sessions
51
58
  (`openchamber.kind === 'review'`) are asked about. Managed Chats (under the
52
59
  `chatRoots` the server passes in) are plain conversations and never in work.
@@ -66,8 +73,10 @@ also exists in VS Code.
66
73
  message id is asked about once.
67
74
  - A turn ended: session assist calls `evaluateTurnEnd` before it arms its quiet
68
75
  window. One call carries only the questions that apply: the open questions
69
- while not in work, `wrap_up` while in work, `recap` / `next_step` for the
70
- assist fields the user has on. No applicable question, no call.
76
+ while not in work, `wrap_up` while in work, `review_ready` while the review offer is on, `recap` / `next_step` for the assist fields
77
+ the user has on. No applicable question, no call.
78
+ - Jev reads the request's first 1500 characters, the answer's first and last
79
+ 500, and three earlier turns cut as routing cuts them.
71
80
 
72
81
  ## Measurement
73
82
 
@@ -82,27 +91,43 @@ across 106 work sessions, 22 of them followed by more edits, and caught 53 of
82
91
  81 shipping steps. At 0.85 the same wording hinted 70 times (18) and missed a
83
92
  closing turn whose answer asked the user for a last check (0.82); the earlier
84
93
  request-only wording at 0.85 hinted 60 times (16) and caught 33.
94
+ Growing the answer excerpt from 300+300 to 500+500 left `wrap_up` where it
95
+ was (80 hints, 22 followed by edits; 53 of 77 shipping steps).
96
+ `review_ready >= 0.7` was set on the 156 sessions and checked on the 190,
97
+ against whether the turn ran an edit tool (Jev sees no tools): with looks
98
+ done winning it hinted after 271 of 362 edit turns and after 14 of 823 turns
99
+ without edits. The lab is `julia-lab/jev-work` (`run-review.mjs`,
100
+ `score-held.mjs`) under `~/projects/openchamber-extensions`.
85
101
  `recap` stayed high (0.86–0.96) after a closing "thanks"/"commit" that followed
86
102
  real work, because it reads the same three turns the recap does. Do not change
87
103
  the wording or thresholds without re-running this measurement.
88
104
 
89
105
  ## Settings
90
106
 
91
- `sessionWorkEnabled` (the block and the actions, default on) and
92
- `sessionWorkAutoOpen` (Jev opens sessions, default on; not offered in VS Code,
93
- and inert without a classification provider). Both are read at every use.
107
+ - `sessionWorkEnabled`: the block and the actions, default on.
108
+ - `sessionWorkAutoOpen`: Jev opens sessions, default on; not offered in VS
109
+ Code, and inert without a classification provider.
110
+ - `sessionReviewOfferEnabled`: the review offer, default off (Settings → Chat →
111
+ Session Assistance); not offered in VS Code, inert without a classification
112
+ provider, independent of the other two.
113
+ - `sessionWorkKeepInGroup`: a tracked session also stays under its project
114
+ group and folders, default off; Recent, Timeline, and Chats still place it
115
+ once.
116
+
117
+ All four are read at every use.
94
118
 
95
119
  ## Runtime parity
96
120
 
97
121
  - Web, desktop, hosted mobile, Capacitor: all of it; the server owns Jev.
98
- - VS Code: no OpenChamber server, so no Jev and no done hint. The block and
122
+ - VS Code: no OpenChamber server, so no Jev, no done hint and no review offer. The block and
99
123
  Track / Done work through the extension's metadata bridge.
100
124
 
101
125
  ## Tests
102
126
 
103
127
  `state.test.js` (patches, reopen rule, hint lifecycle), `runtime.test.js`
104
128
  (send-time open, no repeat asks, ineligible sessions, the combined turn-end
105
- call, a late answer after the next turn started, never closing, failure as
129
+ call, the review offer with and without In work and looks done winning over
130
+ it, a late answer after the next turn started, never closing, failure as
106
131
  unknown), plus the gate cases in
107
132
  `../session-assist/runtime.test.js`, `loadSettledTurns` in
108
133
  `../session-assist/context.test.js`, and `updateSessionMetadata` in
@@ -36,10 +36,31 @@ const TOWARD_CHANGE = {
36
36
  * held-out 70 hints in 106 work sessions (18 followed by more edits) against
37
37
  * 60 (16), and 47 of 81 shipping steps hinted against 33. The threshold then
38
38
  * went from 0.85 to 0.8: 80 hints (22), 53 of 81 shipping steps, so a closing
39
- * turn whose answer hands the user a last check still gets the hint.
39
+ * turn whose answer hands the user a last check still gets the hint. The
40
+ * answer excerpt then grew from 300+300 to 500+500 characters for
41
+ * `review_ready`; re-run with the production request cut, wrap-up held: 80
42
+ * hints (22) against 81 (24), 53 of 77 shipping steps either way.
40
43
  */
41
44
  const WRAP_UP = 'Does this turn close out the work in this conversation: in `request` the user confirms it works or is good, thanks the agent, or asks to commit, push, merge, sync, or release it, without asking for any further change, and `answer` reports that step done?';
42
45
 
46
+ /**
47
+ * Asked with `wrap_up`: the turn handed over changes the user could look over
48
+ * (the composer then offers an AI review or a walkthrough). Measured against
49
+ * whether the turn ran an edit tool, which Jev never sees: on 190 held-out
50
+ * sessions at 0.7, with a looks-done hint taking precedence, it hinted after
51
+ * 271 of 362 edit turns and after 14 of 823 turns without edits. Two other
52
+ * wordings (handed over as done; no criteria) hinted 4-7x more on turns
53
+ * without edits. Most false hints are cleanups outside the code (deleted
54
+ * builds); most misses say "did not change the app" after scratch edits.
55
+ */
56
+ const REVIEW_READY = {
57
+ question: 'Did the agent change the project in this turn, so there are new changes the user could look over now?',
58
+ criteria: {
59
+ true: '`answer` reports that the agent itself edited the project in this turn: implemented, fixed, added, removed, refactored, or rewrote code, UI, docs, tests, or configuration.',
60
+ false: 'The agent only explained, answered a question, investigated, reviewed code or a PR, proposed or planned a change without making it, drafted a message, ran commands to show output, or committed, pushed, or merged changes made earlier; or it failed before changing anything.',
61
+ },
62
+ };
63
+
43
64
  const RECAP = {
44
65
  question: 'Is there substantive work or a finding in this conversation worth a one-line reminder later: something the agent changed, fixed, found out, or a decision that was reached?',
45
66
  criteria: {
@@ -59,6 +80,7 @@ const NEXT_STEP = {
59
80
  /** Open when either passes (measured together). */
60
81
  const OPEN_THRESHOLDS = { change: 0.85, towardChange: 0.9 };
61
82
  const WRAP_UP_THRESHOLD = 0.8;
83
+ const REVIEW_READY_THRESHOLD = 0.7;
62
84
  /** The recap is skipped only when Jev is nearly sure there is nothing to remind. */
63
85
  const RECAP_SKIP_BELOW = 0.3;
64
86
  const NEXT_STEP_THRESHOLD = 0.5;
@@ -81,7 +103,8 @@ export const buildSendRequest = ({ history, request }) => ({
81
103
  /**
82
104
  * The request Jev reads when a turn ended, or null when nothing is asked.
83
105
  * `ask` names the groups: `open` (the session is not in work), `wrapUp` (it
84
- * is), `recap` and `nextStep` (the assist fields the user has on).
106
+ * is), `reviewReady` (the review offer is on), `recap` and `nextStep` (the
107
+ * assist fields the user has on).
85
108
  */
86
109
  export const buildTurnEndRequest = ({ history, request, answer, ask }) => {
87
110
  const questions = {};
@@ -90,6 +113,7 @@ export const buildTurnEndRequest = ({ history, request, answer, ask }) => {
90
113
  questions.toward_change = noul(TURN_END_CONTEXT, TOWARD_CHANGE);
91
114
  }
92
115
  if (ask.wrapUp) questions.wrap_up = noul(TURN_END_CONTEXT, { question: WRAP_UP });
116
+ if (ask.reviewReady) questions.review_ready = noul(TURN_END_CONTEXT, REVIEW_READY);
93
117
  if (ask.recap) questions.recap = noul(TURN_END_CONTEXT, RECAP);
94
118
  if (ask.nextStep) questions.next_step = noul(TURN_END_CONTEXT, NEXT_STEP);
95
119
  if (Object.keys(questions).length === 0) return null;
@@ -112,6 +136,11 @@ export const decideWrapUp = (answers) => {
112
136
  return score !== null && score >= WRAP_UP_THRESHOLD;
113
137
  };
114
138
 
139
+ export const decideReviewReady = (answers) => {
140
+ const score = answerOf(answers, 'review_ready');
141
+ return score !== null && score >= REVIEW_READY_THRESHOLD;
142
+ };
143
+
115
144
  /**
116
145
  * Which assist fields are worth the Small Model. A field Jev did not answer
117
146
  * stays allowed: an unanswered question must not silence the assist.