@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
@@ -558,6 +558,245 @@ describe('canonical GitHub mutations', () => {
558
558
  if (status === 200) expect(response.body.target.number).toBe(7);
559
559
  });
560
560
 
561
+ describe('comments and reviews', () => {
562
+ const pullTarget = { project: { owner: 'acme', name: 'app' }, number: 7 };
563
+ const recordRequests = (respond) => {
564
+ const requests = [];
565
+ vi.stubGlobal('fetch', vi.fn(async (url, options) => {
566
+ const entry = { method: options.method, path: new URL(String(url)).pathname, body: options.body ? JSON.parse(options.body) : null };
567
+ requests.push(entry);
568
+ return respond(entry);
569
+ }));
570
+ return requests;
571
+ };
572
+
573
+ it('comments on a pull request after checking it belongs to the bound repository', async () => {
574
+ const account = await auth.setGitHubAuth({ accessToken: 'token-one', user: { id: 1, login: 'actor' } });
575
+ const requests = recordRequests((entry) => (entry.method === 'GET' ? jsonResponse(pullRequest()) : jsonResponse({ id: 11 }, 201)));
576
+ const { app, store } = makeApp();
577
+
578
+ const response = await request(app).post('/api/source-control/github/pr/comment')
579
+ .send({ ...context(account.accountId, { target: pullTarget }), body: 'Looks good' }).expect(200);
580
+
581
+ expect(response.body.result).toEqual({});
582
+ expect(requests).toEqual([
583
+ { method: 'GET', path: '/repos/acme/app/pulls/7', body: null },
584
+ { method: 'POST', path: '/repos/acme/app/issues/7/comments', body: { body: 'Looks good' } },
585
+ ]);
586
+ expect(store.records.get('request-one')).toMatchObject({ kind: 'change-request-comment', state: 'succeeded' });
587
+ });
588
+
589
+ it('reviews the head commit the client read, with the text as the review body', async () => {
590
+ const account = await auth.setGitHubAuth({ accessToken: 'token-one', user: { id: 1, login: 'actor' } });
591
+ const requests = recordRequests((entry) => (entry.method === 'GET'
592
+ ? jsonResponse(pullRequest())
593
+ : jsonResponse({ id: 12, state: entry.body.event === 'APPROVE' ? 'APPROVED' : 'CHANGES_REQUESTED' })));
594
+ const { app } = makeApp();
595
+ const target = { ...pullTarget, headSha: 'abc123' };
596
+
597
+ const approved = await request(app).post('/api/source-control/github/pr/review')
598
+ .send({ ...context(account.accountId, { target }), verdict: 'approve' }).expect(200);
599
+ const requested = await request(app).post('/api/source-control/github/pr/review')
600
+ .send({ ...context(account.accountId, { target, idempotencyKey: 'request-two' }), verdict: 'request-changes', body: 'Rename it' }).expect(200);
601
+
602
+ expect(approved.body.result).toEqual({ commented: false });
603
+ expect(requested.body.result).toEqual({ commented: true });
604
+ expect(requests.filter((entry) => entry.method === 'POST')).toEqual([
605
+ { method: 'POST', path: '/repos/acme/app/pulls/7/reviews', body: { commit_id: 'abc123', event: 'APPROVE' } },
606
+ { method: 'POST', path: '/repos/acme/app/pulls/7/reviews', body: { commit_id: 'abc123', event: 'REQUEST_CHANGES', body: 'Rename it' } },
607
+ ]);
608
+ });
609
+
610
+ it('refuses a review once the pull request moved past the commit the client read', async () => {
611
+ const account = await auth.setGitHubAuth({ accessToken: 'token-one', user: { id: 1, login: 'actor' } });
612
+ const requests = recordRequests(() => jsonResponse(pullRequest({ head: { ...pullRequest().head, sha: 'def456' } })));
613
+ const { app } = makeApp();
614
+
615
+ const response = await request(app).post('/api/source-control/github/pr/review')
616
+ .send({ ...context(account.accountId, { target: { ...pullTarget, headSha: 'abc123' } }), verdict: 'approve' }).expect(409);
617
+
618
+ expect(response.body.code).toBe('SOURCE_CONTROL_MUTATION_TARGET_CHANGED');
619
+ expect(requests.map((entry) => entry.method)).toEqual(['GET']);
620
+ });
621
+
622
+ it('asks for text before requesting changes and for the reviewed commit, and writes nothing', async () => {
623
+ const account = await auth.setGitHubAuth({ accessToken: 'token-one', user: { id: 1, login: 'actor' } });
624
+ const requests = recordRequests(() => jsonResponse(pullRequest()));
625
+ const { app } = makeApp();
626
+
627
+ await request(app).post('/api/source-control/github/pr/review')
628
+ .send({ ...context(account.accountId, { target: { ...pullTarget, headSha: 'abc123' } }), verdict: 'request-changes', body: ' ' }).expect(400);
629
+ await request(app).post('/api/source-control/github/pr/comment')
630
+ .send({ ...context(account.accountId, { target: pullTarget, idempotencyKey: 'request-two' }) }).expect(400);
631
+ await request(app).post('/api/source-control/github/pr/review')
632
+ .send({ ...context(account.accountId, { target: pullTarget, idempotencyKey: 'request-three' }), verdict: 'approve' }).expect(400);
633
+
634
+ expect(requests).toEqual([]);
635
+ });
636
+
637
+ it("passes GitHub's reason when it refuses a review", async () => {
638
+ const account = await auth.setGitHubAuth({ accessToken: 'token-one', user: { id: 1, login: 'actor' } });
639
+ recordRequests((entry) => (entry.method === 'GET'
640
+ ? jsonResponse(pullRequest())
641
+ : jsonResponse({ message: 'Unprocessable Entity', errors: ['Review Can not approve your own pull request'] }, 422)));
642
+ const { app, store } = makeApp();
643
+
644
+ const response = await request(app).post('/api/source-control/github/pr/review')
645
+ .send({ ...context(account.accountId, { target: { ...pullTarget, headSha: 'abc123' } }), verdict: 'approve' }).expect(422);
646
+
647
+ expect(response.body).toEqual({ error: 'Review Can not approve your own pull request', code: 'SOURCE_CONTROL_MUTATION_REJECTED' });
648
+ expect(store.records.get('request-one')).toMatchObject({ state: 'failed', result: { failureStatus: 422 } });
649
+ });
650
+
651
+ it('comments on an issue and refuses a pull request number on the issue route', async () => {
652
+ const account = await auth.setGitHubAuth({ accessToken: 'token-one', user: { id: 1, login: 'actor' } });
653
+ const requests = recordRequests((entry) => {
654
+ if (entry.method === 'POST') return jsonResponse({ id: 13 }, 201);
655
+ return entry.path.endsWith('/issues/8')
656
+ ? jsonResponse({ number: 8, pull_request: { url: 'https://api.github.com/repos/acme/app/pulls/8' } })
657
+ : jsonResponse({ number: 5 });
658
+ });
659
+ const { app, store } = makeApp();
660
+
661
+ const response = await request(app).post('/api/source-control/github/issues/comment')
662
+ .send({ ...context(account.accountId, { target: { project: pullTarget.project, number: 5 } }), body: 'On it' }).expect(200);
663
+ const refused = await request(app).post('/api/source-control/github/issues/comment')
664
+ .send({ ...context(account.accountId, { target: { project: pullTarget.project, number: 8 }, idempotencyKey: 'request-two' }), body: 'On it' }).expect(409);
665
+
666
+ expect(response.body.target).toEqual({
667
+ repositoryId: 'repo-one', bindingRevision: 3, primaryRemote: 'origin',
668
+ project: { id: 'acme/app', owner: 'acme', name: 'app' }, number: 5,
669
+ });
670
+ expect(refused.body.code).toBe('SOURCE_CONTROL_MUTATION_TARGET_INVALID');
671
+ expect(requests.filter((entry) => entry.method === 'POST')).toEqual([
672
+ { method: 'POST', path: '/repos/acme/app/issues/5/comments', body: { body: 'On it' } },
673
+ ]);
674
+ expect(store.records.get('request-one')).toMatchObject({ kind: 'issue-comment', state: 'succeeded' });
675
+ });
676
+
677
+ it('closes a pull request and reopens an issue, and passes GitHub\'s reason for a merged one', async () => {
678
+ const account = await auth.setGitHubAuth({ accessToken: 'token-one', user: { id: 1, login: 'actor' } });
679
+ const requests = recordRequests((entry) => {
680
+ if (entry.path.endsWith('/pulls/7')) {
681
+ return entry.method === 'GET' ? jsonResponse(pullRequest()) : jsonResponse(pullRequest({ state: 'closed' }));
682
+ }
683
+ if (entry.path.endsWith('/pulls/9')) {
684
+ return entry.method === 'GET'
685
+ ? jsonResponse(pullRequest({ number: 9, html_url: 'https://github.com/acme/app/pull/9', state: 'closed', merged: true }))
686
+ : jsonResponse({ message: 'Validation Failed', errors: [{ message: 'state cannot be changed. The pull request has been merged.' }] }, 422);
687
+ }
688
+ return entry.method === 'GET' ? jsonResponse({ number: 5, state: 'closed' }) : jsonResponse({ number: 5, state: 'open' });
689
+ });
690
+ const { app, store } = makeApp();
691
+
692
+ const closed = await request(app).post('/api/source-control/github/pr/state')
693
+ .send({ ...context(account.accountId, { target: pullTarget }), state: 'closed' }).expect(200);
694
+ const reopened = await request(app).post('/api/source-control/github/issues/state')
695
+ .send({ ...context(account.accountId, { target: { project: pullTarget.project, number: 5 }, idempotencyKey: 'request-two' }), state: 'open' }).expect(200);
696
+ const merged = await request(app).post('/api/source-control/github/pr/state')
697
+ .send({ ...context(account.accountId, { target: { ...pullTarget, number: 9 }, idempotencyKey: 'request-three' }), state: 'open' }).expect(422);
698
+ await request(app).post('/api/source-control/github/pr/state')
699
+ .send({ ...context(account.accountId, { target: pullTarget, idempotencyKey: 'request-four' }), state: 'merged' }).expect(400);
700
+
701
+ expect(closed.body.result).toEqual({ state: 'closed' });
702
+ expect(reopened.body.result).toEqual({ state: 'open' });
703
+ expect(merged.body).toEqual({ error: 'state cannot be changed. The pull request has been merged.', code: 'SOURCE_CONTROL_MUTATION_REJECTED' });
704
+ expect(requests.filter((entry) => entry.method === 'PATCH')).toEqual([
705
+ { method: 'PATCH', path: '/repos/acme/app/pulls/7', body: { state: 'closed' } },
706
+ { method: 'PATCH', path: '/repos/acme/app/issues/5', body: { state: 'open' } },
707
+ { method: 'PATCH', path: '/repos/acme/app/pulls/9', body: { state: 'open' } },
708
+ ]);
709
+ expect(store.records.get('request-two')).toMatchObject({ kind: 'issue-state', state: 'succeeded', result: { state: 'open' } });
710
+ });
711
+
712
+ it.each([
713
+ ['closed', 'succeeded'],
714
+ ['open', 'failed'],
715
+ ])('reconciles an interrupted close from the issue being %s without writing again', async (current, outcome) => {
716
+ const account = await auth.setGitHubAuth({ accessToken: 'token-one', user: { id: 1, login: 'actor' } });
717
+ const target = {
718
+ repositoryId: 'repo-one', bindingRevision: 3, primaryRemote: 'origin',
719
+ project: { id: 'acme/app', owner: 'acme', name: 'app' }, number: 5,
720
+ };
721
+ const actor = { provider: 'github', instance: 'github.com', accountId: account.accountId };
722
+ const input = { kind: 'issue-state', actor, credential: credentialFor(account), target, state: 'closed' };
723
+ const store = makeStore([{ key: 'request-one', inputDigest: digestMutationInput(input), kind: 'issue-state', actor, target, state: 'running' }]);
724
+ const fetch = vi.fn(async () => jsonResponse({ number: 5, state: current }));
725
+ vi.stubGlobal('fetch', fetch);
726
+ const { app } = makeApp({ store });
727
+
728
+ await request(app).post('/api/source-control/github/issues/state')
729
+ .send({ ...context(account.accountId, { target: { project: pullTarget.project, number: 5 } }), state: 'closed' })
730
+ .expect(outcome === 'succeeded' ? 200 : 409);
731
+
732
+ expect(store.records.get('request-one').state).toBe(outcome);
733
+ expect(fetch).toHaveBeenCalledOnce();
734
+ });
735
+
736
+ it('sets labels as one sorted set on pull requests and issues', async () => {
737
+ const account = await auth.setGitHubAuth({ accessToken: 'token-one', user: { id: 1, login: 'actor' } });
738
+ const requests = recordRequests((entry) => {
739
+ if (entry.method === 'PUT') return jsonResponse(entry.body.labels.map((name) => ({ name })));
740
+ return entry.path.endsWith('/pulls/7') ? jsonResponse(pullRequest()) : jsonResponse({ number: 5, state: 'open' });
741
+ });
742
+ const { app, store } = makeApp();
743
+
744
+ await request(app).post('/api/source-control/github/pr/labels')
745
+ .send({ ...context(account.accountId, { target: pullTarget }), labels: ['docs', 'bug', 'docs'] }).expect(200);
746
+ await request(app).post('/api/source-control/github/issues/labels')
747
+ .send({ ...context(account.accountId, { target: { project: pullTarget.project, number: 5 }, idempotencyKey: 'request-two' }), labels: [] }).expect(200);
748
+ await request(app).post('/api/source-control/github/pr/labels')
749
+ .send({ ...context(account.accountId, { target: pullTarget, idempotencyKey: 'request-three' }), labels: [' '] }).expect(400);
750
+
751
+ expect(requests.filter((entry) => entry.method === 'PUT')).toEqual([
752
+ { method: 'PUT', path: '/repos/acme/app/issues/7/labels', body: { labels: ['bug', 'docs'] } },
753
+ { method: 'PUT', path: '/repos/acme/app/issues/5/labels', body: { labels: [] } },
754
+ ]);
755
+ expect(store.records.get('request-two')).toMatchObject({ kind: 'issue-labels', state: 'succeeded' });
756
+ });
757
+
758
+ it('asks and stops asking reviewers so the requested set matches', async () => {
759
+ const account = await auth.setGitHubAuth({ accessToken: 'token-one', user: { id: 1, login: 'actor' } });
760
+ const requests = recordRequests((entry) => {
761
+ if (entry.path.endsWith('/requested_reviewers') && entry.method === 'GET') {
762
+ return jsonResponse({ users: [{ login: 'ann' }, { login: 'bob' }], teams: [] });
763
+ }
764
+ if (entry.path.endsWith('/requested_reviewers')) return jsonResponse(pullRequest());
765
+ return jsonResponse(pullRequest());
766
+ });
767
+ const { app } = makeApp();
768
+
769
+ const response = await request(app).post('/api/source-control/github/pr/reviewers')
770
+ .send({ ...context(account.accountId, { target: pullTarget }), reviewers: ['cid', 'ann'] }).expect(200);
771
+
772
+ expect(response.body.result).toEqual({});
773
+ expect(requests.filter((entry) => entry.method !== 'GET')).toEqual([
774
+ { method: 'POST', path: '/repos/acme/app/pulls/7/requested_reviewers', body: { reviewers: ['cid'] } },
775
+ { method: 'DELETE', path: '/repos/acme/app/pulls/7/requested_reviewers', body: { reviewers: ['bob'] } },
776
+ ]);
777
+ });
778
+
779
+ it('leaves a comment interrupted mid-write as outcome unknown instead of posting it again', async () => {
780
+ const account = await auth.setGitHubAuth({ accessToken: 'token-one', user: { id: 1, login: 'actor' } });
781
+ const target = {
782
+ repositoryId: 'repo-one', bindingRevision: 3, primaryRemote: 'origin',
783
+ project: { id: 'acme/app', owner: 'acme', name: 'app' }, number: 5,
784
+ };
785
+ const actor = { provider: 'github', instance: 'github.com', accountId: account.accountId };
786
+ const input = { kind: 'issue-comment', actor, credential: credentialFor(account), target, body: 'On it' };
787
+ const store = makeStore([{ key: 'request-one', inputDigest: digestMutationInput(input), kind: 'issue-comment', actor, target, state: 'running' }]);
788
+ const fetch = vi.fn(async () => jsonResponse({}));
789
+ vi.stubGlobal('fetch', fetch);
790
+ const { app } = makeApp({ store });
791
+
792
+ const response = await request(app).post('/api/source-control/github/issues/comment')
793
+ .send({ ...context(account.accountId, { target: { project: pullTarget.project, number: 5 } }), body: 'On it' }).expect(409);
794
+
795
+ expect(response.body.code).toBe('SOURCE_CONTROL_MUTATION_OUTCOME_UNKNOWN');
796
+ expect(fetch).not.toHaveBeenCalled();
797
+ });
798
+ });
799
+
561
800
  it('does not retry or invalidate after a definite provider rejection', async () => {
562
801
  const account = await auth.setGitHubAuth({ accessToken: 'token-one', user: { id: 1, login: 'actor' } });
563
802
  const fetch = vi.fn(async () => jsonResponse({ message: 'rejected private payload' }, 422));
@@ -195,7 +195,7 @@ describe('GET /api/source-control/github/references', () => {
195
195
  });
196
196
 
197
197
  const res = await request(app).get('/api/source-control/github/references')
198
- .query({ ...readContext(project), kind: 'issue', filter: 'created', query: 'crash' })
198
+ .query({ ...readContext(project), kind: 'issue', state: 'open', people: 'created', query: 'crash' })
199
199
  .expect(200);
200
200
 
201
201
  expect(res.body).toMatchObject({ connected: true, cursor: 'c1', hasMore: true, total: 31 });
@@ -224,12 +224,14 @@ describe('GET /api/source-control/github/references', () => {
224
224
  number: 4,
225
225
  state: 'OPEN',
226
226
  reviewDecision: 'APPROVED',
227
+ headCommit: { nodes: [] },
228
+ reviewRequests: { nodes: [] },
227
229
  additions: 1,
228
230
  deletions: 2,
229
231
  changedFiles: 1,
230
232
  comments: { totalCount: 0, nodes: [] },
231
233
  reviews: { nodes: [] },
232
- commits: { nodes: [] },
234
+ commits: { totalCount: 0, nodes: [] },
233
235
  } } } }));
234
236
 
235
237
  const detail = await request(app).get('/api/source-control/github/references/detail')
@@ -248,6 +250,32 @@ describe('GET /api/source-control/github/references', () => {
248
250
  expect(graphqlCalls()).toBe(before);
249
251
  });
250
252
 
253
+ it("reads a repository's labels and assignable people for the board's pickers, only from the project repo network", async () => {
254
+ const fetch = vi.fn(async (url) => {
255
+ const endpoint = new URL(url);
256
+ if (endpoint.pathname === '/repos/example/project') return response({ full_name: 'example/project', fork: false });
257
+ if (endpoint.pathname === '/repos/example/project/labels') return response([{ name: 'bug', color: 'd73a4a' }, { name: 'docs', color: '' }]);
258
+ if (endpoint.pathname === '/repos/example/project/assignees') return response([{ login: 'octo', avatar_url: 'https://avatars/octo' }, { login: 'hubot' }]);
259
+ throw new Error(`Unexpected GitHub request: ${endpoint.pathname}`);
260
+ });
261
+ vi.stubGlobal('fetch', fetch);
262
+
263
+ const labels = await request(app).get('/api/source-control/github/references/labels')
264
+ .query({ ...readContext(project), owner: 'example', repo: 'project' }).expect(200);
265
+ const reviewers = await request(app).get('/api/source-control/github/references/reviewers')
266
+ .query({ ...readContext(project), owner: 'example', repo: 'project' }).expect(200);
267
+
268
+ expect(labels.body).toEqual({ connected: true, items: [{ name: 'bug', color: 'd73a4a' }, { name: 'docs' }] });
269
+ expect(reviewers.body).toEqual({ connected: true, items: [
270
+ { id: 'octo', login: 'octo', avatarUrl: 'https://avatars/octo' },
271
+ { id: 'hubot', login: 'hubot' },
272
+ ] });
273
+ const calls = fetch.mock.calls.length;
274
+ await request(app).get('/api/source-control/github/references/labels')
275
+ .query({ ...readContext(project), owner: 'someone', repo: 'else' }).expect(400);
276
+ expect(fetch.mock.calls.slice(calls).some(([url]) => String(url).includes('/labels'))).toBe(false);
277
+ });
278
+
251
279
  it('reads the statuses of listed PRs with the sidebar summaries, only from the project repo network', async () => {
252
280
  const fetch = serveGitHub((body) => {
253
281
  expect(body.variables).toMatchObject({ o0: 'example', n0: 'project', p0: 4, o1: 'example', n1: 'project', p1: 9 });
@@ -306,3 +334,107 @@ describe('GET /api/source-control/github/references', () => {
306
334
  expect(res.body).not.toHaveProperty('items');
307
335
  });
308
336
  });
337
+
338
+
339
+ describe('pull request head for a worktree made by number', () => {
340
+ let github;
341
+ const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-github-pr-head-'));
342
+ const project = path.join(dataDir, 'project');
343
+ const headSha = 'ABCDEF0123456789abcdef0123456789abcdef01';
344
+ const forkPull = (overrides = {}) => ({
345
+ ...pull(42),
346
+ user: { login: 'alice', id: 9 },
347
+ head: {
348
+ ref: 'feature/fix',
349
+ sha: headSha,
350
+ repo: {
351
+ name: 'project',
352
+ owner: { login: 'alice' },
353
+ html_url: 'https://github.com/alice/project',
354
+ clone_url: 'https://github.com/alice/project.git',
355
+ },
356
+ },
357
+ ...overrides,
358
+ });
359
+
360
+ beforeAll(async () => {
361
+ process.env.OPENCHAMBER_DATA_DIR = dataDir;
362
+ fs.mkdirSync(project);
363
+ execFileSync('git', ['init', '-q', project]);
364
+ execFileSync('git', ['-C', project, 'remote', 'add', 'origin', 'https://github.com/example/project.git']);
365
+ const { setGitHubAuth, setGhCliDisabled } = await import('./auth.js');
366
+ setGhCliDisabled(true);
367
+ ({ accountId } = await setGitHubAuth({ accessToken: 'fake-test-token', user: { id: 7, login: 'tester' } }));
368
+ github = registerGitHubRoutes(express(), routeOptions());
369
+ });
370
+
371
+ afterEach(() => {
372
+ vi.unstubAllGlobals();
373
+ vi.restoreAllMocks();
374
+ });
375
+
376
+ afterAll(() => {
377
+ if (previousDataDir === undefined) delete process.env.OPENCHAMBER_DATA_DIR;
378
+ else process.env.OPENCHAMBER_DATA_DIR = previousDataDir;
379
+ fs.rmSync(dataDir, { recursive: true, force: true });
380
+ });
381
+
382
+ const serveGitHub = (pullResponse) => {
383
+ const fetch = vi.fn(async (url) => {
384
+ const endpoint = new URL(url);
385
+ if (endpoint.pathname === '/repos/example/project') return response({ full_name: 'example/project', fork: false });
386
+ if (endpoint.pathname === '/repos/example/project/pulls/42') return pullResponse();
387
+ throw new Error(`Unexpected GitHub request: ${endpoint.pathname}`);
388
+ });
389
+ vi.stubGlobal('fetch', fetch);
390
+ return fetch;
391
+ };
392
+
393
+ it('answers the current account when no gh login is in use', async () => {
394
+ await expect(github.readCurrentAccountId()).resolves.toBe(accountId);
395
+ });
396
+
397
+ // A stand-in `gh` on PATH answers `gh auth token`, as the real CLI does.
398
+ it.skipIf(process.platform === 'win32')('treats only a rejected gh token as no account', async () => {
399
+ const binDirectory = path.join(dataDir, 'bin');
400
+ fs.mkdirSync(binDirectory, { recursive: true });
401
+ fs.writeFileSync(path.join(binDirectory, 'gh'), '#!/bin/sh\necho fake-gh-token\n', { mode: 0o755 });
402
+ const previousPath = process.env.PATH;
403
+ process.env.PATH = `${binDirectory}${path.delimiter}${previousPath}`;
404
+ const { setGhCliActive, setGhCliDisabled } = await import('./auth.js');
405
+ setGhCliDisabled(false);
406
+ setGhCliActive(true);
407
+ try {
408
+ vi.stubGlobal('fetch', vi.fn(async () => response({ message: 'Server Error' }, 500)));
409
+ await expect(github.readCurrentAccountId()).rejects.toMatchObject({ status: 500 });
410
+ vi.stubGlobal('fetch', vi.fn(async () => response({ message: 'Bad credentials' }, 401)));
411
+ await expect(github.readCurrentAccountId()).resolves.toBeNull();
412
+ } finally {
413
+ setGhCliActive(false);
414
+ setGhCliDisabled(true);
415
+ process.env.PATH = previousPath;
416
+ }
417
+ });
418
+
419
+ it('reads the project, head and head owner from the primary remote', async () => {
420
+ serveGitHub(() => response(forkPull()));
421
+ await expect(github.readChangeRequestHead({ context: readContext(project), number: 42 })).resolves.toEqual({
422
+ project: { id: 'example/project', owner: 'example', name: 'project' },
423
+ headSha: headSha.toLowerCase(),
424
+ headBranch: 'feature/fix',
425
+ headOwner: 'alice',
426
+ });
427
+ });
428
+
429
+ it('refuses a pull request whose fork was deleted', async () => {
430
+ serveGitHub(() => response(forkPull({ head: { ref: 'feature/fix', sha: headSha, repo: null } })));
431
+ await expect(github.readChangeRequestHead({ context: readContext(project), number: 42 }))
432
+ .rejects.toMatchObject({ status: 409, message: 'Pull request #42 has no branch to check out: its fork was deleted' });
433
+ });
434
+
435
+ it('refuses a number that is not a pull request there', async () => {
436
+ serveGitHub(() => response({ message: 'Not Found' }, 404));
437
+ await expect(github.readChangeRequestHead({ context: readContext(project), number: 42 }))
438
+ .rejects.toMatchObject({ status: 404, message: 'Pull request #42 was not found in example/project' });
439
+ });
440
+ });
@@ -29,6 +29,9 @@ This module owns GitLab instance validation, authentication, remote resolution,
29
29
  - Resource payloads are converted to provider-neutral DTOs inside this module. Project web and clone URLs must match the selected instance and reported project path. Malformed canonical issue, nested issue identity and label data, issue-note, branch, fork metadata, merge-request list, merge-request note, and diff payloads fail the read rather than becoming partial, empty, or not-fork success. Raw GitBeaker payloads do not cross the server boundary.
30
30
  - CI lookup is partial enrichment. A jobs failure or malformed jobs payload leaves the merge request authoritative and returns `ci: null`.
31
31
  - GitLab supports merge and squash through the shared mutation. Rebase is not advertised because GitLab cannot perform it as the same atomic merge operation.
32
+ - Comments and reviews are canonical mutations too: `/pr/comment` and `/issues/comment` post a note (an issue resolves in the bound project, `resolveIssueMutation`), `/pr/review` approves through the approvals API with the target's `headSha` (a moved head is a target mismatch) or requests changes through GraphQL `mergeRequestRequestChanges` (the REST API has no such call; the requester posts to `../graphql` from the REST base, so the same credential and no-redirect rules apply). GraphQL answers a refusal (not a reviewer, an older GitLab) with errors and status 200; that is a definite `422` failure. GitLab answers an approval it does not allow with `401`, which is reported as `403` so it never invalidates the account. A review's text is a separate note posted after the verdict; if it fails the verdict stands and the result says `commented: false`. Comments and reviews reconcile as `outcome-unknown`.
33
+ - `/pr/state` and `/issues/state` close or reopen through `stateEvent` (`close`/`reopen`) and check the returned state. Restart reconciliation (`reconcileStateMutation`) reads the merge request or issue: the asked state succeeds, the other open/closed state is a definite not-applied failure. GitLab refuses to reopen a merged merge request.
34
+ - `/pr/labels`, `/issues/labels` and `/pr/reviewers` set the whole set through `edit` (`labels` joined by commas, empty clears; `reviewerIds` by numeric user id), deduplicated and sorted for the digest. `reconcileSetMutation` succeeds only when the current set matches, else `outcome-unknown`. `GET /references/labels` and `/references/reviewers` read one page of the project's labels and active members (inherited too) for a project in the bound network. The timeline context carries `reviewers`.
32
35
  - The four merge-request mutation routes are canonical only. They require the complete repository binding context and durable idempotency executor; missing validation or execution support returns `501`. Validation runs before credential lookup, client creation, project resolution, or provider access, and mutation authentication uses only the validated account.
33
36
  - Canonical mutation target resolution starts only from the trusted primary remote and that project's declared fork parent. The expected target project must match one of those projects. Parent lookup failures fail the request. Create accepts `remote` only when it equals the primary remote. `headRemote` defaults to the primary remote; an explicit value resolves that exact remote without ranking or fallback and must map to the already established primary/parent network.
34
37
  - Update, merge, and ready preflight the expected project, merge-request number, head, base, and head SHA against strictly mapped provider state immediately before execution. Mutation preflight, mutation responses, and restart reconciliation also require an authoritative boolean `draft` or `work_in_progress` field. Broader resource reads retain compatibility with older payloads that omit those fields. Durable targets contain the repository and binding revisions, primary remote, server project identity, and actual merge-request coordinates. Digests cover the exact credential account ID and revision, resolved target, and every normalized operation input. Durable records never contain titles, descriptions, provider payloads, tokens, or remote URLs.
@@ -36,7 +39,7 @@ This module owns GitLab instance validation, authentication, remote resolution,
36
39
  - Restart reconciliation reads GitLab and never repeats a mutation. Running records resolve provider coordinates only when reconciliation needs them. Create succeeds only when one merge request exactly matches source project and branch plus target project and base. Update remains outcome-unknown because its private title and description are not persisted. Merge succeeds when GitLab reports merged, and ready succeeds when GitLab reports non-draft; authoritative unchanged states become definite failures where safe. Ambiguous or malformed results remain outcome-unknown.
37
40
  - Cross-process execution exclusion, lock errors, and stopped-writer orphan recovery follow the [source-control lock contract](../source-control/DOCUMENTATION.md#cross-process-locks-and-manual-recovery). A live or crashed owner's execution lock blocks reconciliation; restarting alone does not remove it.
38
41
  - `GET /pr/status` keeps a last-known answer per trusted read context (`source-control/status-cache.js`, 90 s TTL, credential revision in the key). A fresh repeat is answered from it unless `force` is set; a provider round trip is bounded to 12 s; a transient failure (unreachable, 429, 5xx, timeout) answers with the last-known status, or 503 when there is none, so a badge keeps its state through an outage. A refused credential (401) is never answered from it. A successful mutation drops the repository's entries; an invalidated account drops its own. Context, list, and history reads have no caches, and failed mutations do not alter unrelated server state.
39
- - Issue and merge-request list operations return open records for shared pickers. Direct number/context lookup can still resolve closed records.
42
+ - Issue and merge-request lists take the pickers' `state` (`open` → `opened`, `closed`, `merged`, `all`) and `people` (`assigned` → `assigned_to_me`, `created` → `created_by_me`; `reviewRequested` filters merge requests by the account's own username, read once per service). Unknown values read as open and anyone. Direct number/context lookup resolves any record.
40
43
  - Shared picker and sidebar consumers select GitLab only from a matching repository remote. Self-managed hosts must already appear in source-control instance discovery; unknown hosts are never inferred as GitLab.
41
44
 
42
45
  ## Structure
@@ -47,6 +50,7 @@ This module owns GitLab instance validation, authentication, remote resolution,
47
50
  - `verify.js` and `user.js`: token verification and payload parsing.
48
51
  - `glab-credential.js`: host-scoped CLI credential retrieval.
49
52
  - `client.js`: GitBeaker client factory with OpenChamber's own requester: redirects are refused so the credential header never leaves the instance, and only reads retry after 429/502. Mutations are sent once so an ambiguous proxy failure reaches the durable executor.
53
+ - `changeRequestContext` with `includeTimeline` (`/pulls/context?timeline=1`, only the board and picker preview ask) also returns the newest 50 `commits`, oldest first, `commitsComplete: false` when there are more, and `verdicts`: approvals and change requests GitLab records as system notes. Without it the context carries neither and makes no commit request, so attaching a merge request costs what it did.
50
54
  - Resource lists page by GitLab's `x-next-page` header. Notes, diffs, issue comments, and branches are read to completion within 20 pages of 100; past that the read fails rather than returning a capped list as complete. Merges send the preflighted head as `sha`, and GitLab's 409 maps to `SOURCE_CONTROL_MUTATION_TARGET_MISMATCH`. Fork merge requests expose their source project as `headProject`, and CI jobs are read from the pipeline's own project.
51
55
  - `repo.js`: instance-bound remote parsing, ranking, and directory resolution.
52
56
  - `mappers.js`: GitLab payload validation and provider-neutral DTO conversion.
@@ -167,3 +167,4 @@ describe('source-control auth storage', () => {
167
167
  });
168
168
  });
169
169
  });
170
+
@@ -4,7 +4,7 @@ import { parseGitLabRemoteUrl } from './repo.js';
4
4
  const text = (value) => isString(value) ? value : '';
5
5
  const integer = (value) => Number.isInteger(value) && value >= 0 ? value : null;
6
6
 
7
- function mapGitLabUser(value, identity) {
7
+ export function mapGitLabUser(value, identity) {
8
8
  if (!isPlainObject(value) || integer(value.id) === null || !text(value.username)) return null;
9
9
  const user = { ...identity, id: String(value.id), username: value.username };
10
10
  if (text(value.avatar_url)) user.avatarUrl = value.avatar_url;
@@ -125,6 +125,35 @@ export function mapGitLabNote(value, identity, fallbackUrl = '') {
125
125
  return note;
126
126
  }
127
127
 
128
+ // GitLab writes a review verdict as a system note on the merge request.
129
+ const VERDICT_NOTES = new Map([
130
+ ['approved this merge request', 'approved'],
131
+ ['requested changes', 'changes_requested'],
132
+ ]);
133
+
134
+ /** A system note that records an approval or a change request; null for any other note. */
135
+ export function mapGitLabVerdictNote(value, identity, fallbackUrl = '') {
136
+ if (!isPlainObject(value) || value.system !== true || !isString(value.body)) return null;
137
+ const state = VERDICT_NOTES.get(value.body.trim().toLowerCase());
138
+ if (!state) return null;
139
+ const verdict = { state, url: text(value.web_url) || fallbackUrl, createdAt: text(value.created_at) ? value.created_at : null };
140
+ const author = mapGitLabUser(value.author, identity);
141
+ if (author) verdict.author = author;
142
+ return verdict;
143
+ }
144
+
145
+ /** A merge request commit as the preview's timeline shows it. */
146
+ export function mapGitLabCommit(value) {
147
+ if (!isPlainObject(value) || !text(value.id)) return null;
148
+ return {
149
+ sha: value.id,
150
+ headline: text(value.title) || '',
151
+ authorName: text(value.author_name) || null,
152
+ committedAt: text(value.committed_date) || text(value.created_at) || null,
153
+ url: text(value.web_url) || null,
154
+ };
155
+ }
156
+
128
157
  export function mapGitLabDiff(value) {
129
158
  if (!isPlainObject(value) || (!text(value.new_path) && !text(value.old_path))) return null;
130
159
  const file = {