@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
@@ -84,6 +84,16 @@ function sendAuthStorageError(res, error) {
84
84
  return null;
85
85
  }
86
86
 
87
+ /** A list's state and whose items, from the query; unknown values read as open and anyone. */
88
+ function readListFilter(query) {
89
+ const state = requestText(query?.state);
90
+ const people = requestText(query?.people);
91
+ return {
92
+ state: ['open', 'closed', 'merged', 'all'].includes(state) ? state : 'open',
93
+ people: ['any', 'assigned', 'created', 'reviewRequested'].includes(people) ? people : 'any',
94
+ };
95
+ }
96
+
87
97
  function requestText(value) {
88
98
  return isString(value) ? value.trim() : '';
89
99
  }
@@ -420,20 +430,21 @@ export function registerGitLabRoutes(app, options = {}) {
420
430
  record: mutationRecord(context, kind, actor, credential, existing.target, normalized.digest),
421
431
  providerAccountId: resource.providerUserId,
422
432
  perform: () => { throw mutationConflict(); },
423
- reconcile: async () => reconcile(service, await resolveReplay()),
433
+ reconcile: async () => reconcile(service, await resolveReplay(), normalized),
424
434
  classifyError: classifyMutationError,
425
435
  });
426
436
  statusCache.invalidate({ instance: context.instance, accountId: context.accountId, repositoryId: context.repositoryId });
427
437
  return mutationReceipt(execution.record, execution.replayed, resource.providerUserId);
428
438
  }
429
- const resolved = kind === 'change-request-create'
430
- ? await service.resolveCreateMutation(context, context.target, normalized)
431
- : await service.resolveChangeRequestMutation(context, context.target);
439
+ let resolved;
440
+ if (kind === 'change-request-create') resolved = await service.resolveCreateMutation(context, context.target, normalized);
441
+ else if (kind.startsWith('issue-')) resolved = await service.resolveIssueMutation(context, context.target);
442
+ else resolved = await service.resolveChangeRequestMutation(context, context.target);
432
443
  const execution = await options.mutationExecutor.execute({
433
444
  record: mutationRecord(context, kind, actor, credential, resolved.target, normalized.digest),
434
445
  providerAccountId: resource.providerUserId,
435
446
  perform: () => perform(service, normalized, resolved),
436
- reconcile: () => reconcile(service, resolved),
447
+ reconcile: () => reconcile(service, resolved, normalized),
437
448
  classifyError: classifyMutationError,
438
449
  });
439
450
  statusCache.invalidate({ instance: context.instance, accountId: context.accountId, repositoryId: context.repositoryId });
@@ -785,6 +796,156 @@ export function registerGitLabRoutes(app, options = {}) {
785
796
  }
786
797
  });
787
798
 
799
+ // A comment or a review leaves nothing that tells a lost write from one
800
+ // never sent short of guessing from its text, so the outcome stays unknown.
801
+ const unknownOutcome = () => ({ state: 'outcome-unknown' });
802
+ // GitLab's own limit for a note.
803
+ const NOTE_MAX_LENGTH = 1_000_000;
804
+ const noteText = (value, required) => {
805
+ if (value === undefined && !required) return undefined;
806
+ if (!isString(value) || !value.trim() || value.length > NOTE_MAX_LENGTH) {
807
+ throw invalidMutationContext('body is required and must fit a GitLab note');
808
+ }
809
+ return value;
810
+ };
811
+
812
+ app.post('/api/source-control/gitlab/pr/comment', async (req, res) => {
813
+ try {
814
+ const body = req.body ?? {};
815
+ return res.json(await executeMutation(body, 'change-request-comment', (input, context) => {
816
+ if (!context.target.number) throw invalidMutationContext('target number is required');
817
+ const text = noteText(input.body, true);
818
+ return { body: text, digest: { body: text } };
819
+ }, async (service, normalized, resolved) => {
820
+ await service.commentChangeRequest({ providerTarget: resolved.providerTarget, body: normalized.body });
821
+ return {};
822
+ }, unknownOutcome));
823
+ } catch (error) {
824
+ return sendMutationError(res, error);
825
+ }
826
+ });
827
+
828
+ app.post('/api/source-control/gitlab/issues/comment', async (req, res) => {
829
+ try {
830
+ const body = req.body ?? {};
831
+ return res.json(await executeMutation(body, 'issue-comment', (input, context) => {
832
+ if (!context.target.number) throw invalidMutationContext('target number is required');
833
+ const text = noteText(input.body, true);
834
+ return { body: text, digest: { body: text } };
835
+ }, async (service, normalized, resolved) => {
836
+ await service.commentIssue({ providerTarget: resolved.providerTarget, body: normalized.body });
837
+ return {};
838
+ }, unknownOutcome));
839
+ } catch (error) {
840
+ return sendMutationError(res, error);
841
+ }
842
+ });
843
+
844
+ // Close or reopen: `state` is what the item should become.
845
+ for (const [path, kind, method] of [
846
+ ['/api/source-control/gitlab/pr/state', 'change-request-state', 'setChangeRequestState'],
847
+ ['/api/source-control/gitlab/issues/state', 'issue-state', 'setIssueState'],
848
+ ]) {
849
+ app.post(path, async (req, res) => {
850
+ try {
851
+ const body = req.body ?? {};
852
+ return res.json(await executeMutation(body, kind, (input, context) => {
853
+ if (!context.target.number || !['open', 'closed'].includes(input.state)) {
854
+ throw invalidMutationContext('target number and state are required');
855
+ }
856
+ return { state: input.state, digest: { state: input.state } };
857
+ }, (service, normalized, resolved) => service[method]({
858
+ state: normalized.state,
859
+ providerTarget: resolved.providerTarget,
860
+ targetProject: resolved.target.project,
861
+ expectedTarget: resolved.target,
862
+ }), (service, resolved, normalized) => service.reconcileStateMutation(resolved.providerTarget, kind, normalized.state)));
863
+ } catch (error) {
864
+ return sendMutationError(res, error);
865
+ }
866
+ });
867
+ }
868
+
869
+ // Labels and reviewers as one whole set, so a repeat asks for the same result.
870
+ const nameList = (value, limit) => {
871
+ if (!Array.isArray(value) || value.length > limit
872
+ || value.some((entry) => !isString(entry) || !entry.trim() || entry.length > 256)) {
873
+ throw invalidMutationContext('labels or reviewers are invalid');
874
+ }
875
+ return [...new Set(value)].sort();
876
+ };
877
+ for (const [path, kind, field, method] of [
878
+ ['/api/source-control/gitlab/pr/labels', 'change-request-labels', 'labels', 'setLabels'],
879
+ ['/api/source-control/gitlab/issues/labels', 'issue-labels', 'labels', 'setLabels'],
880
+ ['/api/source-control/gitlab/pr/reviewers', 'change-request-reviewers', 'reviewers', 'setReviewers'],
881
+ ]) {
882
+ app.post(path, async (req, res) => {
883
+ try {
884
+ const body = req.body ?? {};
885
+ return res.json(await executeMutation(body, kind, (input, context) => {
886
+ if (!context.target.number) throw invalidMutationContext('target number is required');
887
+ const names = nameList(input[field], field === 'labels' ? 100 : 50);
888
+ // GitLab names a reviewer by user id.
889
+ if (field === 'reviewers' && names.some((id) => !/^\d+$/.test(id))) throw invalidMutationContext('reviewers are invalid');
890
+ return { names, digest: { [field]: names } };
891
+ }, (service, normalized, resolved) => service[method]({
892
+ kind,
893
+ [field]: normalized.names,
894
+ providerTarget: resolved.providerTarget,
895
+ targetProject: resolved.target.project,
896
+ expectedTarget: resolved.target,
897
+ }), (service, resolved, normalized) => service.reconcileSetMutation(resolved.providerTarget, kind, normalized.names)));
898
+ } catch (error) {
899
+ return sendMutationError(res, error);
900
+ }
901
+ });
902
+ }
903
+
904
+ // The project's labels and who can review in it, for the board's pickers.
905
+ for (const [path, method] of [
906
+ ['/api/source-control/gitlab/references/labels', 'listLabels'],
907
+ ['/api/source-control/gitlab/references/reviewers', 'listReviewerCandidates'],
908
+ ]) {
909
+ app.get(path, async (req, res) => {
910
+ try {
911
+ const owner = requestText(req.query?.owner);
912
+ const repo = requestText(req.query?.repo);
913
+ if (!owner || !repo) return res.status(400).json({ error: 'a complete project selector is required' });
914
+ const origin = requestOrigin(req);
915
+ const directory = requestText(req.query?.directory);
916
+ const trustedContext = await validateReadContext(req, origin, directory);
917
+ if (!trustedContext) return res.status(501).json({ error: 'Bound source control status is unavailable' });
918
+ const service = await getResourceService(origin, trustedContext.accountId, true);
919
+ return res.json({ connected: true, items: await service[method](directory, { owner, name: repo }, trustedContext.primaryRemote) });
920
+ } catch (error) {
921
+ if (isReadContextError(error)) return res.status(error.status ?? 400).json(sourceControlErrorBody(error));
922
+ return sendResourceError(res, error);
923
+ }
924
+ });
925
+ }
926
+
927
+ app.post('/api/source-control/gitlab/pr/review', async (req, res) => {
928
+ try {
929
+ const body = req.body ?? {};
930
+ return res.json(await executeMutation(body, 'change-request-review', (input, context) => {
931
+ // A verdict names the commit it is for; without one it would land on whatever was pushed last.
932
+ if (!context.target.number || !context.target.headSha || !['approve', 'request-changes'].includes(input.verdict)) {
933
+ throw invalidMutationContext('target number, head SHA and review verdict are required');
934
+ }
935
+ const text = noteText(input.body, false);
936
+ return { verdict: input.verdict, body: text, digest: { verdict: input.verdict, body: text ?? null } };
937
+ }, (service, normalized, resolved) => service.reviewChangeRequest({
938
+ verdict: normalized.verdict,
939
+ body: normalized.body,
940
+ providerTarget: resolved.providerTarget,
941
+ targetProject: resolved.target.project,
942
+ expectedTarget: resolved.target,
943
+ }), unknownOutcome));
944
+ } catch (error) {
945
+ return sendMutationError(res, error);
946
+ }
947
+ });
948
+
788
949
  app.get('/api/source-control/gitlab/pulls/list', async (req, res) => {
789
950
  try {
790
951
  const origin = requestOrigin(req);
@@ -795,6 +956,7 @@ export function registerGitLabRoutes(app, options = {}) {
795
956
  return res.json(await service.listChangeRequests(directory, {
796
957
  page: requestNumber(req.query?.page) ?? 1,
797
958
  query: requestText(req.query?.query) || undefined,
959
+ ...readListFilter(req.query),
798
960
  remote: trustedContext.primaryRemote,
799
961
  }));
800
962
  } catch (error) {
@@ -818,7 +980,7 @@ export function registerGitLabRoutes(app, options = {}) {
818
980
  if (!number) return res.status(400).json({ error: 'number is required' });
819
981
  const service = await getResourceService(origin, trustedContext.accountId, true);
820
982
  return res.json(await service.changeRequestContext(directory, number, {
821
- includeDiff: req.query?.diff === '1', includeCIDetails: req.query?.checkDetails === '1',
983
+ includeDiff: req.query?.diff === '1', includeCIDetails: req.query?.checkDetails === '1', includeTimeline: req.query?.timeline === '1',
822
984
  project: owner ? { owner, name: repo } : undefined,
823
985
  remote: trustedContext.primaryRemote,
824
986
  constrainToPrimary: true,
@@ -840,6 +1002,7 @@ export function registerGitLabRoutes(app, options = {}) {
840
1002
  const service = await getResourceService(origin, trustedContext.accountId, true);
841
1003
  return res.json(await service.listIssues(directory, {
842
1004
  page: requestNumber(req.query?.page) ?? 1, query: requestText(req.query?.query) || undefined,
1005
+ ...readListFilter(req.query),
843
1006
  remote: trustedContext.primaryRemote,
844
1007
  }));
845
1008
  } catch (error) {
@@ -967,6 +1130,37 @@ export function registerGitLabRoutes(app, options = {}) {
967
1130
  .changeRequestFileContents(context.directory, number, {
968
1131
  project: projectSelector(sourceRepo), remote: context.primaryRemote, path: filePath, previousPath, status,
969
1132
  })),
1133
+ /**
1134
+ * The account GitLab reads on this instance use when none is named: the
1135
+ * glab login while the user has switched to it, else the active account.
1136
+ * Null when neither is there.
1137
+ */
1138
+ readCurrentAccountId: async (origin) => {
1139
+ const instance = await store.readInstance(origin);
1140
+ // A glab login that stopped working leaves the active account, as the status read does.
1141
+ const cli = instance.cliActive ? await getUsableGlab(origin) : null;
1142
+ if (cli) return `${origin}#cli:${cli.user.id}`;
1143
+ return instance.accounts.find((account) => account.id === instance.activeAccountId && account.status !== 'invalid')?.id ?? null;
1144
+ },
1145
+ /** A merge request's head on the context's primary project network, by number, for a worktree made from it. */
1146
+ readChangeRequestHead: async ({ context, number }) => {
1147
+ const trusted = await options.validateReadContext(context);
1148
+ const service = await getResourceService(trusted.instance, trusted.accountId, true);
1149
+ const { changeRequest: request } = await service.changeRequestContext(trusted.directory, number, {
1150
+ remote: trusted.primaryRemote,
1151
+ constrainToPrimary: true,
1152
+ });
1153
+ const headSha = String(request?.headSha || '').toLowerCase();
1154
+ if (!request?.project || !request.head || !/^[0-9a-f]{40}$/.test(headSha)) {
1155
+ throw Object.assign(new Error('GitLab returned an invalid merge request head'), { code: 'MALFORMED_PROVIDER_RESPONSE' });
1156
+ }
1157
+ return {
1158
+ project: { id: request.project.id, owner: request.project.owner, name: request.project.name },
1159
+ headSha,
1160
+ headBranch: request.head,
1161
+ headOwner: (request.headProject ?? request.project).owner,
1162
+ };
1163
+ },
970
1164
  resolveChangeRequestSource: async ({ context, project, number, expectedHeadSha, requestedRemoteName }) => {
971
1165
  const trusted = await options.validateReadContext(context);
972
1166
  const service = await getResourceService(trusted.instance, trusted.accountId, true);
@@ -103,6 +103,20 @@ function canonicalMutationOptions(overrides = {}) {
103
103
  ready: vi.fn(async () => ({ ready: true })),
104
104
  reconcileCreate: vi.fn(async () => ({ state: 'succeeded', result: {} })),
105
105
  reconcileExisting: vi.fn(async () => ({ state: 'outcome-unknown' })),
106
+ comment: vi.fn(async () => undefined),
107
+ commentIssue: vi.fn(async () => undefined),
108
+ review: vi.fn(async () => ({ commented: true })),
109
+ setPullState: vi.fn(async (payload) => ({ state: payload.state })),
110
+ setIssueState: vi.fn(async (payload) => ({ state: payload.state })),
111
+ reconcileState: vi.fn(async (_target, _kind, state) => ({ state: 'succeeded', result: { state } })),
112
+ setLabels: vi.fn(async () => ({})),
113
+ setReviewers: vi.fn(async () => ({})),
114
+ listLabels: vi.fn(async () => [{ name: 'bug', color: 'd73a4a' }]),
115
+ listReviewers: vi.fn(async () => [{ id: '7', login: 'ann' }]),
116
+ resolveIssue: vi.fn(async (context) => ({
117
+ providerTarget: { projectId: '2', number: context.target.number },
118
+ target: { repositoryId: target.repositoryId, bindingRevision: target.bindingRevision, primaryRemote: target.primaryRemote, project: target.project, number: context.target.number },
119
+ })),
106
120
  };
107
121
  const service = {
108
122
  resolveCreateMutation: calls.resolveCreate,
@@ -117,6 +131,18 @@ function canonicalMutationOptions(overrides = {}) {
117
131
  readyChangeRequest: calls.ready,
118
132
  reconcileCreateMutation: calls.reconcileCreate,
119
133
  reconcileChangeRequestMutation: calls.reconcileExisting,
134
+ commentChangeRequest: calls.comment,
135
+ commentIssue: calls.commentIssue,
136
+ reviewChangeRequest: calls.review,
137
+ resolveIssueMutation: calls.resolveIssue,
138
+ setChangeRequestState: calls.setPullState,
139
+ setIssueState: calls.setIssueState,
140
+ reconcileStateMutation: calls.reconcileState,
141
+ setLabels: calls.setLabels,
142
+ setReviewers: calls.setReviewers,
143
+ reconcileSetMutation: vi.fn(async () => ({ state: 'outcome-unknown' })),
144
+ listLabels: calls.listLabels,
145
+ listReviewerCandidates: calls.listReviewers,
120
146
  };
121
147
  return {
122
148
  account,
@@ -403,6 +429,104 @@ describe('GitLab routes', () => {
403
429
  expect(setup.calls.create).toHaveBeenCalledOnce();
404
430
  });
405
431
 
432
+ it('comments, reviews and comments on issues through the canonical executor', async () => {
433
+ const setup = canonicalMutationOptions();
434
+ const app = appWith(setup.options);
435
+ const existing = (key, number, extra = {}) => ({
436
+ ...mutationBody({ idempotencyKey: key, target: { project: { owner: 'team', name: 'repo' }, number, ...extra } }),
437
+ });
438
+
439
+ const commented = await request(app).post('/api/source-control/gitlab/pr/comment')
440
+ .send({ ...existing('comment-one', 5), body: 'Looks good' }).expect(200);
441
+ const reviewed = await request(app).post('/api/source-control/gitlab/pr/review')
442
+ .send({ ...existing('review-one', 5, { headSha: 'abc' }), verdict: 'request-changes', body: 'Rename it' }).expect(200);
443
+ const issue = await request(app).post('/api/source-control/gitlab/issues/comment')
444
+ .send({ ...existing('issue-one', 3), body: 'On it' }).expect(200);
445
+
446
+ expect(commented.body.result).toEqual({});
447
+ expect(reviewed.body.result).toEqual({ commented: true });
448
+ expect(issue.body.target).toMatchObject({ number: 3 });
449
+ expect(setup.calls.comment).toHaveBeenCalledWith({ providerTarget: { projectId: '2', number: 5 }, body: 'Looks good' });
450
+ expect(setup.calls.review).toHaveBeenCalledWith(expect.objectContaining({
451
+ verdict: 'request-changes', body: 'Rename it', expectedTarget: expect.objectContaining({ number: 5, headSha: 'abc' }),
452
+ }));
453
+ expect(setup.calls.commentIssue).toHaveBeenCalledWith({ providerTarget: { projectId: '2', number: 3 }, body: 'On it' });
454
+ expect(setup.calls.resolveIssue).toHaveBeenCalledOnce();
455
+
456
+ await request(app).post('/api/source-control/gitlab/pr/comment').send({ ...existing('comment-two', 5), body: ' ' }).expect(400);
457
+ await request(app).post('/api/source-control/gitlab/pr/review').send({ ...existing('review-two', 5, { headSha: 'abc' }), verdict: 'merge' }).expect(400);
458
+ await request(app).post('/api/source-control/gitlab/pr/review').send({ ...existing('review-three', 5), verdict: 'approve' }).expect(400);
459
+ expect(setup.calls.review).toHaveBeenCalledOnce();
460
+ expect(setup.calls.comment).toHaveBeenCalledOnce();
461
+ });
462
+
463
+ it('closes and reopens through the canonical executor and reconciles by state after a restart', async () => {
464
+ let failCompletion = false;
465
+ const mutationStore = makeMutationStore([], async () => {
466
+ if (failCompletion) {
467
+ failCompletion = false;
468
+ throw new Error('disk unavailable');
469
+ }
470
+ });
471
+ const setup = canonicalMutationOptions({ mutationExecutor: createMutationExecutor({ store: mutationStore }) });
472
+ const app = appWith(setup.options);
473
+ const pull = { ...mutationBody({ idempotencyKey: 'state-one', target: { project: { owner: 'team', name: 'repo' }, number: 5 } }), state: 'closed' };
474
+ const issue = { ...mutationBody({ idempotencyKey: 'state-two', target: { project: { owner: 'team', name: 'repo' }, number: 3 } }), state: 'open' };
475
+
476
+ const closed = await request(app).post('/api/source-control/gitlab/pr/state').send(pull).expect(200);
477
+ failCompletion = true;
478
+ await request(app).post('/api/source-control/gitlab/issues/state').send(issue).expect(409);
479
+ setup.options.mutationExecutor = createMutationExecutor({ store: mutationStore });
480
+ const recovered = await request(appWith(setup.options)).post('/api/source-control/gitlab/issues/state').send(issue).expect(200);
481
+ await request(app).post('/api/source-control/gitlab/pr/state').send({ ...pull, idempotencyKey: 'state-three', state: 'merged' }).expect(400);
482
+
483
+ expect(closed.body.result).toEqual({ state: 'closed' });
484
+ expect(recovered.body).toMatchObject({ replayed: true, result: { state: 'open' } });
485
+ expect(setup.calls.setPullState).toHaveBeenCalledWith(expect.objectContaining({ state: 'closed', providerTarget: { projectId: '2', number: 5 } }));
486
+ expect(setup.calls.setIssueState).toHaveBeenCalledOnce();
487
+ expect(setup.calls.reconcileState).toHaveBeenCalledWith({ projectId: '2', number: 3 }, 'issue-state', 'open');
488
+ });
489
+
490
+ it('sets labels and reviewers as sorted sets and reads the choices for the pickers', async () => {
491
+ const setup = canonicalMutationOptions();
492
+ const app = appWith(setup.options);
493
+ const target = (number) => ({ project: { owner: 'team', name: 'repo' }, number });
494
+
495
+ await request(app).post('/api/source-control/gitlab/pr/labels')
496
+ .send({ ...mutationBody({ idempotencyKey: 'labels-one', target: target(5) }), labels: ['docs', 'bug', 'bug'] }).expect(200);
497
+ await request(app).post('/api/source-control/gitlab/issues/labels')
498
+ .send({ ...mutationBody({ idempotencyKey: 'labels-two', target: target(3) }), labels: [] }).expect(200);
499
+ await request(app).post('/api/source-control/gitlab/pr/reviewers')
500
+ .send({ ...mutationBody({ idempotencyKey: 'reviewers-one', target: target(5) }), reviewers: ['9', '7'] }).expect(200);
501
+ await request(app).post('/api/source-control/gitlab/pr/reviewers')
502
+ .send({ ...mutationBody({ idempotencyKey: 'reviewers-two', target: target(5) }), reviewers: ['ann'] }).expect(400);
503
+
504
+ expect(setup.calls.setLabels).toHaveBeenNthCalledWith(1, expect.objectContaining({ kind: 'change-request-labels', labels: ['bug', 'docs'] }));
505
+ expect(setup.calls.setLabels).toHaveBeenNthCalledWith(2, expect.objectContaining({ kind: 'issue-labels', labels: [], providerTarget: { projectId: '2', number: 3 } }));
506
+ expect(setup.calls.setReviewers).toHaveBeenCalledOnce();
507
+ expect(setup.calls.setReviewers).toHaveBeenCalledWith(expect.objectContaining({ reviewers: ['7', '9'] }));
508
+
509
+ const read = { instance: origin, directory: '/repo', repositoryId: 'repo_one', accountId: `${origin}#9`, bindingRevision: 3, primaryRemote: 'origin', owner: 'team', repo: 'repo' };
510
+ const labels = await request(app).get('/api/source-control/gitlab/references/labels').query(read).expect(200);
511
+ const reviewers = await request(app).get('/api/source-control/gitlab/references/reviewers').query(read).expect(200);
512
+ expect(labels.body).toEqual({ connected: true, items: [{ name: 'bug', color: 'd73a4a' }] });
513
+ expect(reviewers.body).toEqual({ connected: true, items: [{ id: '7', login: 'ann' }] });
514
+ expect(setup.calls.listLabels).toHaveBeenCalledWith('/repo', { owner: 'team', name: 'repo' }, 'origin');
515
+ });
516
+
517
+ it('never posts a comment twice when its outcome was lost', async () => {
518
+ const setup = canonicalMutationOptions();
519
+ setup.calls.comment.mockRejectedValueOnce(Object.assign(new Error('socket closed'), { code: 'ECONNRESET' }));
520
+ const app = appWith(setup.options);
521
+ const body = { ...mutationBody({ target: { project: { owner: 'team', name: 'repo' }, number: 5 } }), body: 'Looks good' };
522
+
523
+ await request(app).post('/api/source-control/gitlab/pr/comment').send(body).expect(409);
524
+ const replay = await request(app).post('/api/source-control/gitlab/pr/comment').send(body).expect(409);
525
+
526
+ expect(replay.body.code).toBe('SOURCE_CONTROL_MUTATION_OUTCOME_UNKNOWN');
527
+ expect(setup.calls.comment).toHaveBeenCalledOnce();
528
+ });
529
+
406
530
  it('reconciles a durable create after restart without repeating the write', async () => {
407
531
  let failCompletion = true;
408
532
  const mutationStore = makeMutationStore([], async () => {
@@ -638,10 +762,11 @@ describe('GitLab routes', () => {
638
762
 
639
763
  expect(createClient).toHaveBeenCalledTimes(2);
640
764
  expect(createClient).toHaveBeenCalledWith({ origin, token: 'bound-token', tokenType: 'oauth' });
641
- expect(listChangeRequests).toHaveBeenCalledWith('/repo', { page: 1, query: undefined, remote: 'upstream' });
765
+ expect(listChangeRequests).toHaveBeenCalledWith('/repo', { page: 1, query: undefined, state: 'open', people: 'any', remote: 'upstream' });
642
766
  expect(changeRequestContext).toHaveBeenCalledWith('/repo', 5, {
643
767
  includeDiff: true,
644
768
  includeCIDetails: true,
769
+ includeTimeline: false,
645
770
  project: { owner: 'team', name: 'repo' },
646
771
  remote: 'upstream',
647
772
  constrainToPrimary: true,
@@ -715,7 +840,7 @@ describe('GitLab routes', () => {
715
840
  expect(createClient).toHaveBeenCalledTimes(5);
716
841
  expect(createClient).toHaveBeenCalledWith({ origin, token: 'bound-token', tokenType: 'oauth' });
717
842
  expect(createResourceService).toHaveBeenCalledWith(expect.objectContaining({ canonicalReads: true }));
718
- expect(listIssues).toHaveBeenCalledWith('/repo', { page: 2, query: 'bug', remote: 'upstream' });
843
+ expect(listIssues).toHaveBeenCalledWith('/repo', { page: 2, query: 'bug', state: 'open', people: 'any', remote: 'upstream' });
719
844
  expect(getIssue).toHaveBeenCalledWith('/repo', 3, { owner: 'team', name: 'repo' }, 'upstream');
720
845
  expect(issueComments).toHaveBeenCalledWith('/repo', 3, { owner: 'team', name: 'repo' }, 'upstream');
721
846
  expect(projectUpstream).toHaveBeenCalledWith('/repo', 'upstream');
@@ -1087,3 +1212,62 @@ describe('GitLab routes', () => {
1087
1212
  expect(active.body).toMatchObject({ connected: true, cli: { available: true, active: true } });
1088
1213
  });
1089
1214
  });
1215
+
1216
+
1217
+ describe('merge request head for a worktree made by number', () => {
1218
+ const account = { id: `${origin}#2`, token: 'bound-token', user: { id: 2 }, source: 'oauth', scope: 'api', status: 'valid' };
1219
+ const register = (changeRequest, store = makeStore(account)) => {
1220
+ const changeRequestContext = vi.fn(async () => ({ changeRequest }));
1221
+ const gitlab = registerGitLabRoutes(express(), {
1222
+ authFile: '/unused/source-control-auth.json',
1223
+ execFile: async () => ({ stdout: '' }),
1224
+ store,
1225
+ createClient: vi.fn(() => ({})),
1226
+ createResourceService: vi.fn(() => ({ changeRequestContext })),
1227
+ validateReadContext: async (context) => ({ ...context, primaryRemote: 'upstream' }),
1228
+ });
1229
+ return { gitlab, changeRequestContext };
1230
+ };
1231
+ const context = {
1232
+ provider: 'gitlab', instance: origin, directory: '/repo', repositoryId: 'repo_one',
1233
+ accountId: account.id, bindingRevision: 3, primaryRemote: 'upstream',
1234
+ };
1235
+
1236
+ it('answers the active account, and none once it is invalid', async () => {
1237
+ const store = makeStore(account);
1238
+ const { gitlab } = register(null, store);
1239
+ await expect(gitlab.readCurrentAccountId(origin)).resolves.toBe(account.id);
1240
+ await store.markAccountInvalid(origin, account.id);
1241
+ await expect(gitlab.readCurrentAccountId(origin)).resolves.toBeNull();
1242
+ });
1243
+
1244
+ it('falls back to the active account when the glab login it was switched to stopped working', async () => {
1245
+ const store = makeStore(account);
1246
+ await store.setCliActive(origin, true);
1247
+ const { gitlab } = register(null, store);
1248
+ await expect(gitlab.readCurrentAccountId(origin)).resolves.toBe(account.id);
1249
+ });
1250
+
1251
+ it('reads the target project and the fork head on the primary project network', async () => {
1252
+ const { gitlab, changeRequestContext } = register({
1253
+ number: 7, head: 'feature/fix', headSha: 'C'.repeat(40),
1254
+ project: { id: '11', owner: 'team', name: 'repo' },
1255
+ headProject: { id: '22', owner: 'alice', name: 'repo' },
1256
+ });
1257
+
1258
+ await expect(gitlab.readChangeRequestHead({ context, number: 7 })).resolves.toEqual({
1259
+ project: { id: '11', owner: 'team', name: 'repo' },
1260
+ headSha: 'c'.repeat(40),
1261
+ headBranch: 'feature/fix',
1262
+ headOwner: 'alice',
1263
+ });
1264
+ expect(changeRequestContext).toHaveBeenCalledExactlyOnceWith('/repo', 7, { remote: 'upstream', constrainToPrimary: true });
1265
+ });
1266
+
1267
+ it('refuses a merge request answer without a usable head', async () => {
1268
+ const { gitlab } = register({ number: 7, head: 'feature/fix', headSha: '', project: { id: '11', owner: 'team', name: 'repo' } });
1269
+
1270
+ await expect(gitlab.readChangeRequestHead({ context, number: 7 }))
1271
+ .rejects.toMatchObject({ code: 'MALFORMED_PROVIDER_RESPONSE' });
1272
+ });
1273
+ });
@@ -1,5 +1 @@
1
- export const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
2
-
3
- export const isPlainObject = (value) => value === Object(value)
4
- && !Array.isArray(value)
5
- && Object.getPrototypeOf(value) === Object.prototype;
1
+ export { isString, isPlainObject } from '../shared/guards.js';
@@ -46,7 +46,7 @@ Folder installs store their realpath with `source: "path"`. ZIP and Git copies l
46
46
 
47
47
  OAuth and pasted tokens live in `{openchamberDataDir}/guest-auth.json`, each entry with the `target` it was minted for (`apiOrigin`, and for OAuth `authorizeUrl` and `tokenUrl`). `takeUsableGuestAuth` runs before every use (proxy, refresh, the status card): tokens whose target differs from what the package declares now, or that carry no target, are dropped on the spot, whatever the grants say. A pending authorization carries the same target; the callback refuses the exchange (`TARGET_CHANGED`) when the package changed its endpoints in between, and the exchange itself goes to the pending target's `tokenUrl`, never to the current manifest's. The client id and secret carry their own `clientTarget`, written by `PUT /api/guests/:id/oauth/client` (`saveGuestOAuthClient`: an empty secret field keeps the stored secret only for the same client id and the same endpoints, decided under the store lock; a new id or moved endpoints drop it); `usableClientCredentials` treats a client entered for other endpoints as absent (`CLIENT_MISSING` on Connect, `hasClient: false` on the card), so a moved package gets no old secret and the user enters new credentials. Every token write that follows a network round trip (the code exchange, a refresh) goes through `patchGuestAuth` with an `expect` predicate checked under the store lock: the entry must still hold the tokens and target the round trip started from, else the answer is dropped and the entry left as it is. The proxy's cleanup after a 401 is scoped the same way: `dropGuestTokens` with an `expect` on the access token the request was refused with, so a refresh that came back empty because the user reconnected meanwhile never erases the new connection. Host Linear tokens stay in `linear-auth.json`. Writes are atomic and `0o600`. The file is per OpenChamber instance, same as `extensions.json`. Web and desktop share it. VS Code and mobile set the guest catalog to `unsupported`. That is not an empty ready list. Those cards stay hidden.
48
48
 
49
- A missing folder is omitted from the list. Other guests stay. A corrupt `extensions.json` is 500, not an empty catalog. A missing or blank `guest-auth.json` is an empty store. Corrupt JSON there is 500, not a disconnected card. This is not Settings → Plugins.
49
+ A missing folder is omitted from the list. Other guests stay. The renderer reads catalog rows one by one (`packages/ui/src/lib/guests/parse.ts`): a row it cannot read, such as one from a newer server with a value this build does not know, shows in Settings → Extensions with its name and Remove, and the other rows still load. An install answered with such a row is reported as added, and the list reloads to show it. A corrupt `extensions.json` is 500, not an empty catalog. A missing or blank `guest-auth.json` is an empty store. Corrupt JSON there is 500, not a disconnected card. This is not Settings → Plugins.
50
50
 
51
51
  ## Invariants
52
52
 
@@ -10,7 +10,7 @@ import {
10
10
 
11
11
  import { dropGuestTokens, getGuestAuth, patchGuestAuth } from './auth-store.js';
12
12
 
13
- export const PENDING_AUTHORIZATION_TTL_MS = 10 * 60_000;
13
+ const PENDING_AUTHORIZATION_TTL_MS = 10 * 60_000;
14
14
 
15
15
  const pendingByState = new Map();
16
16
  const pendingByGuestId = new Map();
@@ -262,7 +262,7 @@ export const storedTokensUsable = (stored, integration) => (
262
262
  * the package names now. Anything else is treated as missing.
263
263
  * @returns {{ clientId: string, clientSecret: string }}
264
264
  */
265
- export const usableClientCredentials = (stored, integration) => {
265
+ const usableClientCredentials = (stored, integration) => {
266
266
  if (!sameTarget(stored?.clientTarget, credentialTarget(integration))) {
267
267
  return { clientId: '', clientSecret: '' };
268
268
  }
@@ -504,7 +504,7 @@ const INHERITED_SERVICE_ENV_NAMES = new Set([
504
504
  ]);
505
505
 
506
506
  /** @param {NodeJS.ProcessEnv} source */
507
- export const inheritedServiceEnv = (source) => {
507
+ const inheritedServiceEnv = (source) => {
508
508
  /** @type {Record<string, string>} */
509
509
  const env = {};
510
510
  for (const [name, value] of Object.entries(source)) {
@@ -23,7 +23,7 @@ export const expandHomePath = (value, home = process.env.HOME || os.homedir()) =
23
23
  * @param {NodeJS.Platform} [platform]
24
24
  * @param {string} [home]
25
25
  */
26
- export const platformSocketCandidates = (
26
+ const platformSocketCandidates = (
27
27
  binding,
28
28
  platform = process.platform,
29
29
  home = process.env.HOME || os.homedir(),
@@ -0,0 +1,8 @@
1
+ export const INJECTED_ENV_KEY: 'OPENCHAMBER_INJECTED_ENV';
2
+
3
+ export function injectedEnvKeys(env: Record<string, string | undefined>): Set<string>;
4
+
5
+ export function assignInjectedEnv<T extends Record<string, string | undefined>>(
6
+ env: T,
7
+ values: Record<string, string | undefined>,
8
+ ): T;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Variables OpenChamber puts into an environment other processes inherit: its
3
+ * own host process (the desktop app, `serve --foreground`), the managed
4
+ * OpenCode server, and through them the integrated terminal and an agent's
5
+ * shell. Every such write goes through `assignInjectedEnv`, which also records
6
+ * the variable's name in `OPENCHAMBER_INJECTED_ENV`. A command run from one of
7
+ * those shells (`openchamber startup enable`) reads the record to tell
8
+ * OpenChamber's configuration from what the user exported, with no list of
9
+ * names that could drift from the code that sets them (#4604).
10
+ *
11
+ * A write that leaves the value as it was is not recorded: the shell already
12
+ * had that value, so it is the user's, and a user's `OPENCODE_PASSWORD` that
13
+ * OpenChamber passes through to the managed OpenCode stays theirs.
14
+ */
15
+
16
+ export const INJECTED_ENV_KEY = 'OPENCHAMBER_INJECTED_ENV';
17
+
18
+ /**
19
+ * Names recorded in `env`, plus the record itself. Empty when nothing was.
20
+ * @param {Record<string, string | undefined>} env
21
+ * @returns {Set<string>}
22
+ */
23
+ export function injectedEnvKeys(env) {
24
+ const record = env[INJECTED_ENV_KEY] ?? '';
25
+ if (record.length === 0) return new Set();
26
+ const keys = new Set(record.split(',').filter((key) => key.length > 0));
27
+ keys.add(INJECTED_ENV_KEY);
28
+ return keys;
29
+ }
30
+
31
+ /**
32
+ * Set each defined value in `env` and record the names whose value changed.
33
+ * @template {Record<string, string | undefined>} T
34
+ * @param {T} env
35
+ * @param {Record<string, string | undefined>} values
36
+ * @returns {T}
37
+ */
38
+ export function assignInjectedEnv(env, values) {
39
+ const recorded = injectedEnvKeys(env);
40
+ recorded.delete(INJECTED_ENV_KEY);
41
+ for (const [key, value] of Object.entries(values)) {
42
+ if (value === undefined || env[key] === value) continue;
43
+ env[key] = value;
44
+ recorded.add(key);
45
+ }
46
+ if (recorded.size > 0) {
47
+ env[INJECTED_ENV_KEY] = [...recorded].join(',');
48
+ }
49
+ return env;
50
+ }