@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
@@ -1,4 +1,4 @@
1
- import { mapGitLabCI, mapGitLabDiff, mapGitLabIssue, mapGitLabMergeRequest, mapGitLabNote, mapGitLabProject } from './mappers.js';
1
+ import { mapGitLabCI, mapGitLabCommit, mapGitLabDiff, mapGitLabIssue, mapGitLabMergeRequest, mapGitLabNote, mapGitLabProject, mapGitLabUser, mapGitLabVerdictNote } from './mappers.js';
2
2
  import { resolveGitLabProjectsFromDirectory } from './repo.js';
3
3
  import { isPlainObject, isString } from './validation.js';
4
4
 
@@ -14,6 +14,11 @@ const PER_PAGE = 20;
14
14
  // end within this bound; past it the read fails instead of passing a capped
15
15
  // list off as complete.
16
16
  const COLLECTION_PER_PAGE = 100;
17
+ // List filters: the picker's names to GitLab's.
18
+ const LIST_STATES = { open: 'opened', closed: 'closed', merged: 'merged', all: 'all' };
19
+ const LIST_SCOPES = { any: 'all', assigned: 'assigned_to_me', created: 'created_by_me' };
20
+ // The preview's timeline shows the newest commits only.
21
+ const TIMELINE_COMMIT_LIMIT = 50;
17
22
  const COLLECTION_MAX_PAGES = 20;
18
23
 
19
24
  function requireMapped(value, message) {
@@ -197,9 +202,30 @@ export function createGitLabResourceService({ origin, client, resolveProjects =
197
202
  };
198
203
  const constrainedProject = async (...args) => (await constrainedProjectNetwork(...args)).target;
199
204
 
205
+ // The account's own user, read once: review requests are filtered by name.
206
+ let currentUsername = null;
207
+ const readCurrentUsername = async () => {
208
+ currentUsername ??= client.Users.showCurrentUser().then((user) => {
209
+ if (!isPlainObject(user) || !isString(user.username) || !user.username.trim()) throw new Error('GitLab returned an invalid current user');
210
+ return user.username;
211
+ });
212
+ return currentUsername;
213
+ };
214
+
215
+ /** A list's state and whose items, as GitLab's list parameters. */
216
+ const listFilter = async (options, kind) => {
217
+ const state = LIST_STATES[options.state] ?? 'opened';
218
+ const people = options.people ?? 'any';
219
+ if (people === 'reviewRequested') {
220
+ return kind === 'pull' ? { state, scope: 'all', reviewerUsername: await readCurrentUsername() } : { state, scope: 'all' };
221
+ }
222
+ return { state: kind === 'issue' && state === 'merged' ? 'closed' : state, scope: LIST_SCOPES[people] ?? 'all' };
223
+ };
224
+
200
225
  const listProjectMRs = (projectId, options) => client.MergeRequests.all({
201
226
  projectId,
202
- scope: 'all',
227
+ scope: options.scope ?? 'all',
228
+ reviewerUsername: options.reviewerUsername,
203
229
  maxPages: 1,
204
230
  perPage: options.perPage ?? 100,
205
231
  page: options.page ?? 1,
@@ -391,6 +417,36 @@ export function createGitLabResourceService({ origin, client, resolveProjects =
391
417
  };
392
418
  };
393
419
 
420
+ const postNote = async (create) => {
421
+ const raw = await create();
422
+ if (!isPlainObject(raw) || !Number.isInteger(raw.id)) throw new Error('GitLab returned an invalid note');
423
+ };
424
+
425
+ // Requesting changes exists only in GitLab's GraphQL API. The path is
426
+ // resolved against the REST base (/api/v4/), so it reaches /api/graphql
427
+ // through the same credential and no-redirect requester.
428
+ const REQUEST_CHANGES_MUTATION = `mutation($path: ID!, $iid: String!) {
429
+ mergeRequestRequestChanges(input: { projectPath: $path, iid: $iid }) {
430
+ mergeRequest { iid }
431
+ errors
432
+ }
433
+ }`;
434
+ const requestChanges = async (project, number) => {
435
+ const response = await client.MergeRequests.requester.post('../graphql', {
436
+ body: { query: REQUEST_CHANGES_MUTATION, variables: { path: `${project.owner}/${project.name}`, iid: String(number) } },
437
+ });
438
+ const payload = response?.body;
439
+ const result = payload?.data?.mergeRequestRequestChanges;
440
+ const errors = [
441
+ ...(Array.isArray(payload?.errors) ? payload.errors.map((error) => error?.message) : []),
442
+ ...(Array.isArray(result?.errors) ? result.errors : []),
443
+ ].filter((message) => isString(message) && message.trim());
444
+ // GraphQL answers a refusal (not a reviewer, an older GitLab) with 200
445
+ // and errors: nothing was changed.
446
+ if (errors.length) throw Object.assign(new Error(errors.join('; ')), { status: 422 });
447
+ if (!isPlainObject(result?.mergeRequest)) throw new Error('GitLab returned an invalid request-changes result');
448
+ };
449
+
394
450
  const resolveMutationActionTarget = async (payload) => {
395
451
  if (payload.providerTarget) {
396
452
  return {
@@ -432,6 +488,149 @@ export function createGitLabResourceService({ origin, client, resolveProjects =
432
488
 
433
489
  resolveChangeRequestMutation: readMutationRequest,
434
490
 
491
+ async resolveIssueMutation(context, expected) {
492
+ if (expected.head !== undefined || expected.base !== undefined || expected.headSha !== undefined) {
493
+ throw Object.assign(new Error('GitLab issue target is invalid'), { code: 'INVALID_SOURCE_CONTROL_MUTATION_CONTEXT', status: 400 });
494
+ }
495
+ const target = await constrainedProject(context.directory, expected.project, context.primaryRemote);
496
+ const issue = requireIssue(await client.Issues.show(expected.number, { projectId: target.project.id }), target.project);
497
+ if (issue.number !== expected.number) throw new Error('GitLab returned an invalid issue');
498
+ return {
499
+ providerTarget: { projectId: target.project.id, number: issue.number },
500
+ target: resolvedMutationTarget(context, target.project, { number: issue.number }),
501
+ };
502
+ },
503
+
504
+ /** Close or reopen; GitLab refuses to reopen a merged merge request. */
505
+ async setChangeRequestState(payload) {
506
+ const { projectId, number } = payload.providerTarget;
507
+ const raw = await client.MergeRequests.edit(projectId, number, { stateEvent: payload.state === 'closed' ? 'close' : 'reopen' });
508
+ const request = requireMutationResponse(raw, payload.targetProject, payload.providerTarget, payload.expectedTarget);
509
+ if (request.state !== payload.state) throw new Error('GitLab returned an unexpected merge request state');
510
+ return { state: request.state };
511
+ },
512
+
513
+ async setIssueState(payload) {
514
+ const { projectId, number } = payload.providerTarget;
515
+ const raw = await client.Issues.edit(projectId, number, { stateEvent: payload.state === 'closed' ? 'close' : 'reopen' });
516
+ const issue = requireIssue(raw, payload.targetProject);
517
+ if (issue.number !== number || issue.state !== payload.state) throw new Error('GitLab returned an unexpected issue state');
518
+ return { state: issue.state };
519
+ },
520
+
521
+ /** One page of the project's labels, for the board's picker. */
522
+ async listLabels(directory, selector, remote) {
523
+ const target = await constrainedProject(directory, selector, remote);
524
+ const raw = await client.ProjectLabels.all(target.project.id, { perPage: 100, maxPages: 1 });
525
+ if (!Array.isArray(raw)) throw new Error('GitLab returned an invalid label list');
526
+ return raw.flatMap((label) => {
527
+ if (!isPlainObject(label) || !isString(label.name) || !label.name) return [];
528
+ return [isString(label.color) && label.color ? { name: label.name, color: label.color.replace(/^#/, '') } : { name: label.name }];
529
+ });
530
+ },
531
+
532
+ /** One page of the project's active members, inherited ones too: who can be asked to review. */
533
+ async listReviewerCandidates(directory, selector, remote) {
534
+ const target = await constrainedProject(directory, selector, remote);
535
+ const raw = await client.ProjectMembers.all(target.project.id, { includeInherited: true, perPage: 100, maxPages: 1 });
536
+ if (!Array.isArray(raw)) throw new Error('GitLab returned an invalid member list');
537
+ return raw.flatMap((member) => {
538
+ const user = member?.state === 'active' ? mapGitLabUser(member, identity) : null;
539
+ if (!user) return [];
540
+ return [user.avatarUrl ? { id: user.id, login: user.username, avatarUrl: user.avatarUrl } : { id: user.id, login: user.username }];
541
+ });
542
+ },
543
+
544
+ /** The whole label set; an empty one clears them. */
545
+ async setLabels(payload) {
546
+ const { projectId, number } = payload.providerTarget;
547
+ const update = { labels: payload.labels.join(',') };
548
+ const raw = payload.kind === 'issue-labels'
549
+ ? await client.Issues.edit(projectId, number, update)
550
+ : await client.MergeRequests.edit(projectId, number, update);
551
+ if (!isPlainObject(raw) || raw.iid !== number) throw new Error('GitLab returned an invalid edit result');
552
+ return {};
553
+ },
554
+
555
+ /** The whole reviewer set, by user id; an empty one removes them all. */
556
+ async setReviewers(payload) {
557
+ const { projectId, number } = payload.providerTarget;
558
+ const raw = await client.MergeRequests.edit(projectId, number, { reviewerIds: payload.reviewers.map(Number) });
559
+ requireMutationResponse(raw, payload.targetProject, payload.providerTarget, payload.expectedTarget);
560
+ return {};
561
+ },
562
+
563
+ /** Only a match is certain: another edit since may have moved the set either way. */
564
+ async reconcileSetMutation(providerTarget, kind, names) {
565
+ const { projectId, number } = providerTarget;
566
+ const raw = kind === 'issue-labels'
567
+ ? await client.Issues.show(number, { projectId })
568
+ : await client.MergeRequests.show(projectId, number);
569
+ const current = kind === 'change-request-reviewers'
570
+ ? (Array.isArray(raw?.reviewers) ? raw.reviewers.map((user) => String(user?.id)) : null)
571
+ : (Array.isArray(raw?.labels) ? raw.labels.map((label) => (isString(label) ? label : label?.name)) : null);
572
+ if (!current || current.length !== names.length || !current.every((entry) => names.includes(entry))) return { state: 'outcome-unknown' };
573
+ return { state: 'succeeded', result: {} };
574
+ },
575
+
576
+ async reconcileStateMutation(providerTarget, kind, state) {
577
+ const { projectId, number } = providerTarget;
578
+ const project = (await showProject(projectId)).project;
579
+ const current = kind === 'issue-state'
580
+ ? requireIssue(await client.Issues.show(number, { projectId }), project).state
581
+ : requireMutationMergeRequest(await client.MergeRequests.show(projectId, number), project).state;
582
+ if (current === state) return { state: 'succeeded', result: { state } };
583
+ if (current === 'open' || current === 'closed') {
584
+ return { state: 'failed', result: { failureStatus: 409, failureCode: 'SOURCE_CONTROL_MUTATION_NOT_APPLIED' } };
585
+ }
586
+ return { state: 'outcome-unknown' };
587
+ },
588
+
589
+ commentChangeRequest(payload) {
590
+ const { projectId, number } = payload.providerTarget;
591
+ return postNote(() => client.MergeRequestNotes.create(projectId, number, payload.body));
592
+ },
593
+
594
+ commentIssue(payload) {
595
+ const { projectId, number } = payload.providerTarget;
596
+ return postNote(() => client.IssueNotes.create(projectId, number, payload.body));
597
+ },
598
+
599
+ /**
600
+ * The verdict first, then its text as a note: GitLab has no review that
601
+ * carries both. A verdict that lands with a note that does not is still
602
+ * a verdict, reported as `commented: false` so the text is not lost.
603
+ */
604
+ async reviewChangeRequest(payload) {
605
+ const { projectId, number } = payload.providerTarget;
606
+ if (payload.verdict === 'approve') {
607
+ try {
608
+ // The approval is for the commit the user read.
609
+ await client.MergeRequestApprovals.approve(projectId, number, { sha: payload.expectedTarget.headSha });
610
+ } catch (error) {
611
+ const status = requestStatus(error);
612
+ if (status === 409) {
613
+ throw Object.assign(new Error('GitLab merge request headSha changed'), {
614
+ code: 'SOURCE_CONTROL_MUTATION_TARGET_MISMATCH', status: 409,
615
+ });
616
+ }
617
+ // GitLab answers an approval it does not allow (your own merge
618
+ // request, one already approved) with 401; the account is fine.
619
+ if (status === 401) throw Object.assign(new Error('GitLab did not allow this approval'), { status: 403 });
620
+ throw error;
621
+ }
622
+ } else {
623
+ await requestChanges(payload.targetProject, number);
624
+ }
625
+ if (payload.body === undefined) return { commented: false };
626
+ try {
627
+ await postNote(() => client.MergeRequestNotes.create(projectId, number, payload.body));
628
+ return { commented: true };
629
+ } catch {
630
+ return { commented: false };
631
+ }
632
+ },
633
+
435
634
  async reconcileCreateMutation(providerTarget, target) {
436
635
  const payload = readMergeRequestList(await listProjectMRs(providerTarget.targetProjectId, {
437
636
  state: 'all', sourceBranch: target.head, perPage: 100,
@@ -598,7 +797,7 @@ export function createGitLabResourceService({ origin, client, resolveProjects =
598
797
  const targets = await expandTargets([resolved.projects[0]], { requireComplete: canonicalReads });
599
798
  const target = targets.at(-1);
600
799
  const page = readPage(await listProjectMRs(target.project.id, {
601
- state: 'opened', page: options.page ?? 1, perPage: PER_PAGE, search: options.query, showExpanded: true,
800
+ ...await listFilter(options, 'pull'), page: options.page ?? 1, perPage: PER_PAGE, search: options.query, showExpanded: true,
602
801
  }));
603
802
  const rawItems = readMergeRequestList(page.items, target.project);
604
803
  const items = rawItems.map((raw) => canonicalReads
@@ -612,7 +811,7 @@ export function createGitLabResourceService({ origin, client, resolveProjects =
612
811
  constrainToPrimary: options.constrainToPrimary,
613
812
  remote: options.remote,
614
813
  });
615
- const [notes, diffs, ci, headProject] = await Promise.all([
814
+ const [notes, diffs, ci, headProject, commitPage] = await Promise.all([
616
815
  readCollection((page) => client.MergeRequestNotes.all(target.project.id, number, page), 'GitLab merge request has more notes than OpenChamber reads'),
617
816
  options.includeDiff
618
817
  ? readCollection((page) => client.MergeRequests.allDiffs(target.project.id, number, page), 'GitLab merge request has more changed files than OpenChamber reads')
@@ -622,6 +821,11 @@ export function createGitLabResourceService({ origin, client, resolveProjects =
622
821
  Number.isInteger(raw?.source_project_id) && raw.source_project_id !== target.raw?.id
623
822
  ? showProject(raw.source_project_id).then((item) => item.project)
624
823
  : Promise.resolve(null),
824
+ // The newest commits, newest first as GitLab lists them; only the
825
+ // preview's timeline asks.
826
+ options.includeTimeline
827
+ ? client.MergeRequests.allCommits(target.project.id, number, { maxPages: 1, perPage: TIMELINE_COMMIT_LIMIT, showExpanded: true }).then(readPage)
828
+ : Promise.resolve(null),
625
829
  ]);
626
830
  if (canonicalReads && !Array.isArray(notes)) throw new Error('GitLab returned an invalid merge request note list');
627
831
  if (canonicalReads && !Array.isArray(diffs)) throw new Error('GitLab returned an invalid merge request diff list');
@@ -652,7 +856,7 @@ export function createGitLabResourceService({ origin, client, resolveProjects =
652
856
  ? requireMergeRequest(raw, target.project)
653
857
  : requireMapped(mapGitLabMergeRequest(raw, identity, target.project), 'GitLab returned an invalid merge request');
654
858
  if (headProject) changeRequest.headProject = headProject;
655
- return {
859
+ const context = {
656
860
  identity,
657
861
  project: target.project,
658
862
  changeRequest,
@@ -662,13 +866,33 @@ export function createGitLabResourceService({ origin, client, resolveProjects =
662
866
  diff: files.map((file) => file.patch).filter(Boolean).join('\n'),
663
867
  ci,
664
868
  };
869
+ if (!options.includeTimeline) return context;
870
+ if (canonicalReads && !Array.isArray(commitPage?.items)) throw new Error('GitLab returned an invalid merge request commit list');
871
+ return {
872
+ ...context,
873
+ commits: asArray(commitPage?.items).flatMap((commit) => {
874
+ const mapped = mapGitLabCommit(commit);
875
+ return mapped ? [mapped] : [];
876
+ }).reverse(),
877
+ // More than one page means the list above is only the newest ones.
878
+ commitsComplete: !commitPage?.hasMore,
879
+ // Who is asked to review, for the preview's reviewer picker.
880
+ reviewers: asArray(raw.reviewers).flatMap((user) => {
881
+ const mapped = mapGitLabUser(user, identity);
882
+ return mapped ? [mapped] : [];
883
+ }),
884
+ verdicts: asArray(notes).flatMap((note) => {
885
+ const mapped = mapGitLabVerdictNote(note, identity, raw.web_url);
886
+ return mapped ? [mapped] : [];
887
+ }),
888
+ };
665
889
  },
666
890
 
667
891
  async listIssues(directory, options = {}) {
668
892
  const resolved = await resolveDirectoryProjects(directory, options.remote);
669
893
  const target = resolved.projects[0];
670
894
  const page = readPage(await client.Issues.all({
671
- projectId: target.project.id, scope: 'all', state: 'opened', page: options.page ?? 1,
895
+ projectId: target.project.id, ...await listFilter(options, 'issue'), page: options.page ?? 1,
672
896
  perPage: PER_PAGE, maxPages: 1, search: options.query, showExpanded: true,
673
897
  }));
674
898
  if (canonicalReads && !Array.isArray(page.items)) throw new Error('GitLab returned an invalid issue list');
@@ -86,6 +86,52 @@ describe('GitLab resource service', () => {
86
86
  await expect(result).rejects.toThrow('invalid merge request');
87
87
  });
88
88
 
89
+ it('lists by state and by whose items, reading the user once for review requests', async () => {
90
+ const all = list([openMR]);
91
+ const showCurrentUser = vi.fn(async () => ({ id: 3, username: 'sam' }));
92
+ const issuesAll = list([]);
93
+ const { service } = setup({
94
+ MergeRequests: { ...setup().client.MergeRequests, all },
95
+ Users: { showCurrentUser },
96
+ Issues: { ...setup().client.Issues, all: issuesAll },
97
+ });
98
+ await service.listChangeRequests('/repo', { state: 'merged', people: 'created' });
99
+ expect(all.mock.calls.at(-1)?.[0]).toMatchObject({ state: 'merged', scope: 'created_by_me' });
100
+ await service.listChangeRequests('/repo', { state: 'all', people: 'reviewRequested' });
101
+ await service.listChangeRequests('/repo', { people: 'reviewRequested' });
102
+ expect(all.mock.calls.at(-1)?.[0]).toMatchObject({ state: 'opened', scope: 'all', reviewerUsername: 'sam' });
103
+ expect(showCurrentUser).toHaveBeenCalledTimes(1);
104
+ await service.listIssues('/repo', { state: 'merged', people: 'assigned' });
105
+ expect(issuesAll.mock.calls.at(-1)?.[0]).toMatchObject({ state: 'closed', scope: 'assigned_to_me' });
106
+ });
107
+
108
+ it('adds commits and review verdicts only when the timeline asks', async () => {
109
+ const commits = [
110
+ { id: 'bbb', title: 'second', author_name: 'Sam', committed_date: '2026-10-02T10:00:00Z', web_url: `${origin}/team/repo/-/commit/bbb` },
111
+ { id: 'aaa', title: 'first', author_name: 'Sam', committed_date: '2026-10-01T10:00:00Z', web_url: `${origin}/team/repo/-/commit/aaa` },
112
+ ];
113
+ const notes = [
114
+ { id: 4, body: 'Looks good', author: { id: 3, username: 'sam' } },
115
+ { id: 5, body: 'approved this merge request', system: true, created_at: '2026-10-02T11:00:00Z', author: { id: 3, username: 'sam' } },
116
+ { id: 6, body: 'added 1 commit', system: true, author: { id: 3, username: 'sam' } },
117
+ ];
118
+ const allCommits = list(commits);
119
+ const { service } = setup({
120
+ MergeRequests: { ...setup().client.MergeRequests, allCommits, show: vi.fn(async () => ({ ...openMR, reviewers: [{ id: 7, username: 'ann' }, { username: 'no-id' }] })) },
121
+ MergeRequestNotes: { all: list(notes) },
122
+ });
123
+ const plain = await service.changeRequestContext('/repo', 5, {});
124
+ expect(plain).not.toHaveProperty('commits');
125
+ expect(allCommits).not.toHaveBeenCalled();
126
+
127
+ const result = await service.changeRequestContext('/repo', 5, { includeTimeline: true });
128
+ expect(result.commits.map((commit) => commit.headline)).toEqual(['first', 'second']);
129
+ expect(result.commitsComplete).toBe(true);
130
+ expect(result.verdicts).toMatchObject([{ state: 'approved', createdAt: '2026-10-02T11:00:00Z', author: { username: 'sam' } }]);
131
+ expect(result.issueComments.map((comment) => comment.body)).toEqual(['Looks good']);
132
+ expect(result.reviewers).toEqual([expect.objectContaining({ id: '7', username: 'ann' })]);
133
+ });
134
+
89
135
  it('keeps merge-request context when CI jobs fail', async () => {
90
136
  const { service } = setup({ Jobs: { all: vi.fn(async () => { throw new Error('jobs unavailable'); }) } });
91
137
  const result = await service.changeRequestContext('/repo', 5, { includeDiff: true, includeCIDetails: true });
@@ -545,6 +591,139 @@ describe('GitLab resource service', () => {
545
591
  });
546
592
  });
547
593
 
594
+ describe('GitLab comments and reviews', () => {
595
+ const review = (overrides = {}) => ({
596
+ providerTarget: { projectId: '2', number: 5 },
597
+ targetProject: { id: '2', owner: 'team', name: 'repo' },
598
+ expectedTarget: { project: { id: '2', owner: 'team', name: 'repo' }, number: 5, headSha: 'abc' },
599
+ verdict: 'approve',
600
+ ...overrides,
601
+ });
602
+ const notes = (create) => ({ MergeRequestNotes: { ...setup().client.MergeRequestNotes, create } });
603
+
604
+ it('approves the commit the user read, then posts the text as a note', async () => {
605
+ const approve = vi.fn(async () => ({}));
606
+ const create = vi.fn(async () => ({ id: 7 }));
607
+ const { service } = setup({ MergeRequestApprovals: { approve }, ...notes(create) });
608
+
609
+ await expect(service.reviewChangeRequest(review({ body: 'Nice' }))).resolves.toEqual({ commented: true });
610
+ await expect(service.reviewChangeRequest(review())).resolves.toEqual({ commented: false });
611
+
612
+ expect(approve).toHaveBeenCalledWith('2', 5, { sha: 'abc' });
613
+ expect(create).toHaveBeenCalledOnce();
614
+ expect(create).toHaveBeenCalledWith('2', 5, 'Nice');
615
+ });
616
+
617
+ it('keeps the verdict when its note fails and says the text did not post', async () => {
618
+ const create = vi.fn(async () => { throw Object.assign(new Error('locked'), { cause: { response: { status: 403 } } }); });
619
+ const { service } = setup({ MergeRequestApprovals: { approve: vi.fn(async () => ({})) }, ...notes(create) });
620
+
621
+ await expect(service.reviewChangeRequest(review({ body: 'Nice' }))).resolves.toEqual({ commented: false });
622
+ });
623
+
624
+ it('turns an approval GitLab does not allow into a refusal, not a lost sign-in', async () => {
625
+ const refused = Object.assign(new Error('401 Unauthorized'), { cause: { response: { status: 401 } } });
626
+ const moved = Object.assign(new Error('SHA does not match'), { cause: { response: { status: 409 } } });
627
+ const own = setup({ MergeRequestApprovals: { approve: vi.fn(async () => { throw refused; }) } });
628
+ const late = setup({ MergeRequestApprovals: { approve: vi.fn(async () => { throw moved; }) } });
629
+
630
+ await expect(own.service.reviewChangeRequest(review())).rejects.toMatchObject({ status: 403 });
631
+ await expect(late.service.reviewChangeRequest(review())).rejects.toMatchObject({ code: 'SOURCE_CONTROL_MUTATION_TARGET_MISMATCH', status: 409 });
632
+ });
633
+
634
+ it('requests changes through GraphQL and reports its errors as a refusal', async () => {
635
+ const post = vi.fn(async () => ({ body: { data: { mergeRequestRequestChanges: { mergeRequest: { iid: '5' }, errors: [] } } } }));
636
+ const { service } = setup({ MergeRequests: { ...setup().client.MergeRequests, requester: { post } } });
637
+
638
+ await expect(service.reviewChangeRequest(review({ verdict: 'request-changes' }))).resolves.toEqual({ commented: false });
639
+ expect(post).toHaveBeenCalledWith('../graphql', {
640
+ body: { query: expect.stringContaining('mergeRequestRequestChanges'), variables: { path: 'team/repo', iid: '5' } },
641
+ });
642
+
643
+ post.mockResolvedValueOnce({ body: { data: { mergeRequestRequestChanges: { mergeRequest: null, errors: ['Reviewer not found'] } } } });
644
+ await expect(service.reviewChangeRequest(review({ verdict: 'request-changes' }))).rejects.toMatchObject({ message: 'Reviewer not found', status: 422 });
645
+ post.mockResolvedValueOnce({ body: { data: null, errors: [{ message: "Field 'mergeRequestRequestChanges' doesn't exist" }] } });
646
+ await expect(service.reviewChangeRequest(review({ verdict: 'request-changes' }))).rejects.toMatchObject({ status: 422 });
647
+ });
648
+
649
+ it('reads labels and active members, and sets labels and reviewers as whole sets', async () => {
650
+ const editMR = vi.fn(async (_id, _iid, update) => ({ ...openMR, labels: update.labels?.split(',') ?? [] }));
651
+ const editIssue = vi.fn(async () => ({ iid: 3, title: 'Bug', web_url: `${origin}/me/repo/-/issues/3`, state: 'opened' }));
652
+ const { service } = setup({
653
+ MergeRequests: { ...setup().client.MergeRequests, edit: editMR, show: vi.fn(async () => ({ ...openMR, labels: ['bug'], reviewers: [{ id: 7, username: 'ann' }] })) },
654
+ Issues: { ...setup().client.Issues, edit: editIssue },
655
+ ProjectLabels: { all: vi.fn(async () => [{ name: 'bug', color: '#d73a4a' }, { name: '' }]) },
656
+ ProjectMembers: { all: vi.fn(async () => [
657
+ { id: 7, username: 'ann', avatar_url: `${origin}/a.png`, state: 'active' },
658
+ { id: 8, username: 'gone', state: 'blocked' },
659
+ ]) },
660
+ });
661
+ const project = { id: '2', owner: 'team', name: 'repo' };
662
+
663
+ await expect(service.listLabels('/repo', { owner: 'team', name: 'repo' })).resolves.toEqual([{ name: 'bug', color: 'd73a4a' }]);
664
+ await expect(service.listReviewerCandidates('/repo', { owner: 'team', name: 'repo' }))
665
+ .resolves.toEqual([{ id: '7', login: 'ann', avatarUrl: `${origin}/a.png` }]);
666
+
667
+ await service.setLabels({ kind: 'change-request-labels', labels: ['bug', 'docs'], providerTarget: { projectId: '2', number: 5 } });
668
+ await service.setLabels({ kind: 'issue-labels', labels: [], providerTarget: { projectId: '1', number: 3 } });
669
+ await service.setReviewers({
670
+ reviewers: ['7'], providerTarget: { projectId: '2', number: 5 }, targetProject: project, expectedTarget: { project, number: 5 },
671
+ });
672
+ expect(editMR).toHaveBeenNthCalledWith(1, '2', 5, { labels: 'bug,docs' });
673
+ expect(editIssue).toHaveBeenCalledWith('1', 3, { labels: '' });
674
+ expect(editMR).toHaveBeenNthCalledWith(2, '2', 5, { reviewerIds: [7] });
675
+
676
+ await expect(service.reconcileSetMutation({ projectId: '2', number: 5 }, 'change-request-labels', ['bug'])).resolves.toMatchObject({ state: 'succeeded' });
677
+ await expect(service.reconcileSetMutation({ projectId: '2', number: 5 }, 'change-request-reviewers', ['7'])).resolves.toMatchObject({ state: 'succeeded' });
678
+ await expect(service.reconcileSetMutation({ projectId: '2', number: 5 }, 'change-request-labels', ['docs'])).resolves.toEqual({ state: 'outcome-unknown' });
679
+ });
680
+
681
+ it('closes and reopens through stateEvent and reconciles from the current state', async () => {
682
+ const editMR = vi.fn(async () => ({ ...openMR, state: 'closed' }));
683
+ const editIssue = vi.fn(async () => ({ iid: 3, title: 'Bug', web_url: `${origin}/me/repo/-/issues/3`, state: 'opened' }));
684
+ const { service } = setup({
685
+ MergeRequests: { ...setup().client.MergeRequests, edit: editMR },
686
+ Issues: { ...setup().client.Issues, edit: editIssue },
687
+ });
688
+ const project = { id: '2', owner: 'team', name: 'repo' };
689
+
690
+ await expect(service.setChangeRequestState({
691
+ state: 'closed', providerTarget: { projectId: '2', number: 5 }, targetProject: project, expectedTarget: { project, number: 5 },
692
+ })).resolves.toEqual({ state: 'closed' });
693
+ await expect(service.setIssueState({
694
+ state: 'open', providerTarget: { projectId: '1', number: 3 }, targetProject: { id: '1', owner: 'me', name: 'repo' },
695
+ })).resolves.toEqual({ state: 'open' });
696
+ expect(editMR).toHaveBeenCalledWith('2', 5, { stateEvent: 'close' });
697
+ expect(editIssue).toHaveBeenCalledWith('1', 3, { stateEvent: 'reopen' });
698
+
699
+ await expect(service.reconcileStateMutation({ projectId: '2', number: 5 }, 'change-request-state', 'open'))
700
+ .resolves.toEqual({ state: 'succeeded', result: { state: 'open' } });
701
+ await expect(service.reconcileStateMutation({ projectId: '1', number: 3 }, 'issue-state', 'closed'))
702
+ .resolves.toMatchObject({ state: 'failed' });
703
+ });
704
+
705
+ it('comments on merge requests and on issues in the bound project', async () => {
706
+ const mrNote = vi.fn(async () => ({ id: 8 }));
707
+ const issueNote = vi.fn(async () => ({ id: 9 }));
708
+ const { service, client } = setup({ ...notes(mrNote), IssueNotes: { ...setup().client.IssueNotes, create: issueNote } });
709
+ const context = { directory: '/repo', repositoryId: 'repo_one', bindingRevision: 3, primaryRemote: 'origin' };
710
+
711
+ const resolved = await service.resolveIssueMutation(context, { project: { owner: 'me', name: 'repo' }, number: 3 });
712
+ await service.commentIssue({ providerTarget: resolved.providerTarget, body: 'On it' });
713
+ await service.commentChangeRequest({ providerTarget: { projectId: '2', number: 5 }, body: 'Looks good' });
714
+
715
+ expect(resolved).toEqual({
716
+ providerTarget: { projectId: '1', number: 3 },
717
+ target: { repositoryId: 'repo_one', bindingRevision: 3, primaryRemote: 'origin', project: { id: '1', owner: 'me', name: 'repo' }, number: 3 },
718
+ });
719
+ expect(client.Issues.show).toHaveBeenCalledWith(3, { projectId: '1' });
720
+ expect(issueNote).toHaveBeenCalledWith('1', 3, 'On it');
721
+ expect(mrNote).toHaveBeenCalledWith('2', 5, 'Looks good');
722
+ await expect(service.resolveIssueMutation(context, { project: { owner: 'me', name: 'repo' }, number: 3, headSha: 'abc' }))
723
+ .rejects.toMatchObject({ code: 'INVALID_SOURCE_CONTROL_MUTATION_CONTEXT' });
724
+ });
725
+ });
726
+
548
727
  describe('GitLab merge request comparison reads', () => {
549
728
  const diffRefs = { base_sha: 'a'.repeat(40), head_sha: 'b'.repeat(40), start_sha: 'c'.repeat(40) };
550
729