@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
@@ -55,6 +55,16 @@ const createService = (overrides = {}) => {
55
55
  };
56
56
 
57
57
  describe('OpenChamber control service', () => {
58
+ it('creates a target task with inherited selection and accepts retargeting on update', async () => {
59
+ const { service, scheduledTaskService } = createService();
60
+ scheduledTaskService.upsert.mockImplementation(async (_project, task) => ({ task }));
61
+ const created = await service.execute('schedule.create', { name: 'Reuse', prompt: 'Continue', daily: '09:00', targetSessionId: 'ses_target' }, '/repo');
62
+ expect(created.task).toMatchObject({ targetSessionId: 'ses_target', execution: { useDefaults: true } });
63
+ scheduledTaskService.list.mockResolvedValue([{ ...created.task, id: 'task-1' }]);
64
+ await service.execute('schedule.update', { taskId: 'task-1', targetSessionId: 'ses_second', agent: 'plan' }, '/repo');
65
+ expect(scheduledTaskService.upsert.mock.calls[1][1]).toMatchObject({ targetSessionId: 'ses_second', execution: { agent: 'plan' } });
66
+ expect(scheduledTaskService.upsert.mock.calls[1][1].execution.useDefaults).toBeUndefined();
67
+ });
58
68
  it('serves project and model projections without an HTTP or CLI round trip', async () => {
59
69
  const { service } = createService();
60
70
  await expect(service.execute('projects.list')).resolves.toEqual({
@@ -86,6 +96,46 @@ describe('OpenChamber control service', () => {
86
96
  }));
87
97
  });
88
98
 
99
+ it('updates a scheduled task in place, changing only the named fields', async () => {
100
+ const { service, scheduledTaskService } = createService();
101
+ const existing = {
102
+ id: 'task-1',
103
+ name: 'Nightly',
104
+ enabled: true,
105
+ schedule: { kind: 'daily', times: ['09:00'], timezone: 'UTC' },
106
+ execution: { prompt: 'Old', providerID: 'a', modelID: 'm', variant: 'high', agent: 'build' },
107
+ state: { lastRunAt: 5, lastStatus: 'success' },
108
+ };
109
+ scheduledTaskService.list.mockResolvedValue([existing]);
110
+ scheduledTaskService.upsert.mockImplementation(async (_project, task) => ({ task, created: false }));
111
+
112
+ await service.execute('schedule.update', { taskId: 'task-1', prompt: 'New', model: 'b/n', cron: '0 * * * *' }, '/repo');
113
+ const saved = scheduledTaskService.upsert.mock.calls[0][1];
114
+ expect(saved).toEqual({
115
+ id: 'task-1',
116
+ name: 'Nightly',
117
+ enabled: true,
118
+ schedule: { kind: 'cron', cron: '0 * * * *' },
119
+ execution: { prompt: 'New', providerID: 'b', modelID: 'n', agent: 'build' },
120
+ });
121
+ // No state in the patch: the stored run history is kept.
122
+ expect(saved.state).toBeUndefined();
123
+
124
+ await service.execute('schedule.update', { taskId: 'task-1', timezone: 'Europe/Kyiv', disabled: true }, '/repo');
125
+ expect(scheduledTaskService.upsert.mock.calls[1][1]).toMatchObject({
126
+ enabled: false,
127
+ schedule: { kind: 'daily', times: ['09:00'], timezone: 'Europe/Kyiv' },
128
+ execution: existing.execution,
129
+ });
130
+ });
131
+
132
+ it('refuses to update a missing task or one driven by a loop file', async () => {
133
+ const { service, scheduledTaskService } = createService();
134
+ await expect(service.execute('schedule.update', { taskId: 'nope', prompt: 'x' }, '/repo')).rejects.toThrow('Scheduled task not found');
135
+ scheduledTaskService.list.mockResolvedValue([{ id: 'loop:project:x', loopFile: '/repo/.agents/loops/x.md', name: 'x', execution: {}, schedule: {} }]);
136
+ await expect(service.execute('schedule.update', { taskId: 'loop:project:x', prompt: 'x' }, '/repo')).rejects.toThrow('change that file instead');
137
+ });
138
+
89
139
  it('does not combine an explicit schedule project with the tool context directory', async () => {
90
140
  const { service, scheduledTaskService } = createService();
91
141
  await service.execute('schedule.list', { projectId: ' project-1 ' }, '/current-session');
@@ -112,6 +162,15 @@ describe('OpenChamber control service', () => {
112
162
  expect(scheduledTaskService.setEnabled).toHaveBeenCalledWith('project-1', 'task-1', false);
113
163
  });
114
164
 
165
+ it('reports the enabled state that was saved, not the one requested', async () => {
166
+ const { service, scheduledTaskService } = createService();
167
+ scheduledTaskService.setEnabled.mockResolvedValue({ id: 'task-1', enabled: false });
168
+ await expect(service.execute('schedule.toggle', { taskId: 'task-1', disabled: false }, '/repo')).resolves.toEqual({
169
+ task: { id: 'task-1', enabled: false },
170
+ enabled: false,
171
+ });
172
+ });
173
+
115
174
  it('returns an actionable taskId error before resolving schedule scope', async () => {
116
175
  const { service, scheduledTaskService } = createService();
117
176
  await expect(service.execute('schedule.run', {}, '/repo')).rejects.toThrow('taskId is required');
@@ -131,6 +190,23 @@ describe('OpenChamber control service', () => {
131
190
  expect(sessionService.create).toHaveBeenCalledWith({ directory: '/repo', title: 'From tool' });
132
191
  });
133
192
 
193
+ it('hands a pull request number to session creation with the folder and branch names it may carry', async () => {
194
+ const { service, sessionService } = createService();
195
+ await service.execute('session.create', { pullRequest: 42 }, '/repo');
196
+ await service.execute('session.create', { pullRequest: 42, worktree: ' review ', branch: 'review-42' }, '/repo');
197
+ expect(sessionService.create.mock.calls).toEqual([
198
+ [{ directory: '/repo', worktree: {}, pullRequest: 42 }],
199
+ [{ directory: '/repo', worktree: { name: 'review', branchName: 'review-42' }, pullRequest: 42 }],
200
+ ]);
201
+ });
202
+
203
+ it('refuses a pull request number on a send or fork, which would ignore it', async () => {
204
+ const { service, sessionService } = createService();
205
+ await expect(service.execute('session.send', { sessionId: 'ses_1', prompt: 'Go', pullRequest: 42 }, '/repo'))
206
+ .rejects.toThrow('pullRequest applies only to session.create');
207
+ expect(sessionService.send).not.toHaveBeenCalled();
208
+ });
209
+
134
210
  it.each([
135
211
  ['session.send', 'send'],
136
212
  ['session.fork', 'fork'],
@@ -313,6 +389,96 @@ describe('OpenChamber control service', () => {
313
389
  });
314
390
  });
315
391
 
392
+ it('follows the message cursor so all: true returns the whole history', async () => {
393
+ const { service, client } = createService();
394
+ const page = (data, next) => ({ data, cursor: { next } });
395
+ // Newest page first (the API pages with order: 'desc'), then older pages
396
+ // reachable only through cursor.next.
397
+ client.message.list.mockImplementation(async (input) => {
398
+ if (!input.cursor) {
399
+ return page([
400
+ { id: 'msg_new_user', type: 'user', time: { created: 40 }, text: 'Recent question' },
401
+ { id: 'msg_new_assistant', type: 'assistant', time: { created: 50, completed: 55 }, content: [{ type: 'text', text: 'Recent answer' }] },
402
+ ], 'cursor-page-2');
403
+ }
404
+ if (input.cursor === 'cursor-page-2') {
405
+ return page([
406
+ { id: 'msg_old_user', type: 'user', time: { created: 10 }, text: 'Older requirement' },
407
+ ], undefined);
408
+ }
409
+ throw new Error(`unexpected cursor ${input.cursor}`);
410
+ });
411
+
412
+ const result = await service.execute('session.messages', {
413
+ sessionId: 'ses_1',
414
+ directory: '/repo',
415
+ role: 'all',
416
+ all: true,
417
+ });
418
+
419
+ expect(client.message.list).toHaveBeenCalledTimes(2);
420
+ expect(client.message.list.mock.calls[1][0]).toMatchObject({ sessionID: 'ses_1', cursor: 'cursor-page-2' });
421
+ expect(result.messages.map((message) => message.id)).toEqual([
422
+ 'msg_old_user', 'msg_new_user', 'msg_new_assistant',
423
+ ]);
424
+ });
425
+
426
+ it('pages a bounded read with the cursor alone until it has enough text messages', async () => {
427
+ const { service, client } = createService();
428
+ const page = (data, next) => ({ data, cursor: { next } });
429
+ client.message.list.mockImplementation(async (input) => {
430
+ if (!input.cursor) {
431
+ return page([
432
+ { id: 'msg_new_assistant', type: 'assistant', time: { created: 50, completed: 55 }, content: [{ type: 'text', text: 'Recent answer' }] },
433
+ { id: 'msg_new_user', type: 'user', time: { created: 40 }, text: 'Recent question' },
434
+ ], 'cursor-page-2');
435
+ }
436
+ if (input.cursor === 'cursor-page-2') {
437
+ return page([
438
+ { id: 'msg_mid_user', type: 'user', time: { created: 20 }, text: 'Earlier question' },
439
+ { id: 'msg_old_user', type: 'user', time: { created: 10 }, text: 'First question' },
440
+ ], 'cursor-page-3');
441
+ }
442
+ throw new Error(`unexpected cursor ${input.cursor}`);
443
+ });
444
+
445
+ const result = await service.execute('session.messages', {
446
+ sessionId: 'ses_1',
447
+ directory: '/repo',
448
+ role: 'user',
449
+ limit: 2,
450
+ });
451
+
452
+ expect(client.message.list).toHaveBeenCalledTimes(2);
453
+ expect(client.message.list.mock.calls[0][0]).toMatchObject({ sessionID: 'ses_1', order: 'desc' });
454
+ expect(client.message.list.mock.calls[1][0]).toMatchObject({ sessionID: 'ses_1', cursor: 'cursor-page-2' });
455
+ expect(client.message.list.mock.calls[1][0]).not.toHaveProperty('order');
456
+ expect(result.messages.map((message) => message.id)).toEqual(['msg_mid_user', 'msg_new_user']);
457
+ });
458
+
459
+ it('stops paging when the message cursor repeats or a page comes back empty', async () => {
460
+ const { service, client } = createService();
461
+ client.message.list.mockImplementation(async (input) => {
462
+ if (!input.cursor) {
463
+ return { data: [{ id: 'msg_new_user', type: 'user', time: { created: 20 }, text: 'Question' }], cursor: { next: 'cursor-loop' } };
464
+ }
465
+ return { data: [{ id: 'msg_old_user', type: 'user', time: { created: 10 }, text: 'Older question' }], cursor: { next: 'cursor-loop' } };
466
+ });
467
+
468
+ const looped = await service.execute('session.messages', { sessionId: 'ses_1', directory: '/repo', all: true });
469
+ expect(client.message.list).toHaveBeenCalledTimes(2);
470
+ expect(looped.messages.map((message) => message.id)).toEqual(['msg_old_user', 'msg_new_user']);
471
+
472
+ client.message.list.mockReset();
473
+ client.message.list.mockImplementation(async (input) => (input.cursor
474
+ ? { data: [], cursor: { next: `${input.cursor}-next` } }
475
+ : { data: [{ id: 'msg_user', type: 'user', time: { created: 10 }, text: 'Question' }], cursor: { next: 'cursor-empty' } }));
476
+
477
+ const drained = await service.execute('session.messages', { sessionId: 'ses_1', directory: '/repo', all: true });
478
+ expect(client.message.list).toHaveBeenCalledTimes(2);
479
+ expect(drained.messages.map((message) => message.id)).toEqual(['msg_user']);
480
+ });
481
+
316
482
  it('rejects actions outside the fixed contract', async () => {
317
483
  const { service } = createService();
318
484
  await expect(service.execute('session.delete')).rejects.toThrow('Unsupported OpenChamber action');
@@ -11,17 +11,12 @@
11
11
  * does not mention keeps whatever OpenCode says.
12
12
  */
13
13
 
14
+ import { asNonEmptyString } from '../shared/guards.js';
14
15
  import fsDefault from 'node:fs';
15
16
  import pathDefault from 'node:path';
16
17
 
17
18
  const ARCHIVE_FILE_NAME = 'sessions-archive.json';
18
19
 
19
- const asNonEmptyString = (value) => {
20
- if (typeof value !== 'string') return null;
21
- const trimmed = value.trim();
22
- return trimmed.length > 0 ? trimmed : null;
23
- };
24
-
25
20
  const asTimestamp = (value) => (Number.isSafeInteger(value) && value > 0 ? value : null);
26
21
 
27
22
  const asIdList = (ids) => {
@@ -1,3 +1,4 @@
1
+ import { asNonEmptyString } from '../shared/guards.js';
1
2
  import express from 'express';
2
3
  import {
3
4
  createWorktree as createWorktreeDefault,
@@ -16,12 +17,6 @@ import { applyForkInheritance } from './fork-inheritance.js';
16
17
  import { createOpenCodeClient as defaultCreateOpenCodeClient } from './opencode-client.js';
17
18
  import { createSessionMetadataStore, createOpenCodeSessionMetadata } from './session-metadata-store.js';
18
19
 
19
- const asNonEmptyString = (value) => {
20
- if (typeof value !== 'string') return null;
21
- const trimmed = value.trim();
22
- return trimmed.length > 0 ? trimmed : null;
23
- };
24
-
25
20
  const asList = (value) => (Array.isArray(value) ? value : []);
26
21
 
27
22
  const splitModel = (value) => {
@@ -253,7 +248,6 @@ const applySessionSelection = async ({ client, sessionID, model, agent, variant
253
248
  const createSession = async ({ client, directory, title }) => {
254
249
  const session = await client.session.create({
255
250
  location: { directory },
256
- metadata: { openchamber: { adopted: true } },
257
251
  ...(title ? { title } : {}),
258
252
  });
259
253
  const sessionID = asNonEmptyString(session?.id);
@@ -389,6 +383,27 @@ const resolveRequestedDirectory = async ({ payload, readSettingsFromDiskMigrated
389
383
  const WORKTREE_BOOTSTRAP_TIMEOUT_MS = 60_000;
390
384
  const WORKTREE_BOOTSTRAP_POLL_MS = 150;
391
385
 
386
+ /**
387
+ * A worktree from a pull or merge request: its number, and the folder and
388
+ * branch names when the caller chose them. The head, the fork and its remote
389
+ * come from the provider; a start ref or upstream would contradict them.
390
+ */
391
+ const resolvePullRequestInput = (payload) => {
392
+ if (payload?.pullRequest === undefined) return null;
393
+ if (!Number.isSafeInteger(payload.pullRequest) || payload.pullRequest < 1) {
394
+ throw new OpenChamberControlError('pullRequest must be a pull or merge request number, such as 42', 400);
395
+ }
396
+ if (payload.worktree?.startRef !== undefined || payload.setUpstream !== undefined) {
397
+ throw new OpenChamberControlError('pullRequest checks out the pull request\'s own branch; do not combine it with startRef or setUpstream', 400);
398
+ }
399
+ const input = { number: payload.pullRequest };
400
+ const name = asNonEmptyString(payload.worktree?.name);
401
+ if (name) input.name = name;
402
+ const branchName = asNonEmptyString(payload.worktree?.branchName);
403
+ if (branchName) input.branchName = branchName;
404
+ return input;
405
+ };
406
+
392
407
  const resolveWorktreeInput = (payload) => {
393
408
  if (!payload?.worktree || typeof payload.worktree !== 'object') return null;
394
409
  const name = asNonEmptyString(payload.worktree.name);
@@ -418,6 +433,9 @@ export const createOpenChamberSessionService = (dependencies) => {
418
433
  sessionKnowledgeRuntime = null,
419
434
  worktreeBootstrapStore,
420
435
  hydrateWorktreeCheckout,
436
+ // Creates a worktree from a pull or merge request through the Git routes'
437
+ // contributor pipeline. Absent where contributor worktrees are not served.
438
+ createChangeRequestWorktree = null,
421
439
  dataDir = null,
422
440
  archiveStore: injectedArchiveStore = null,
423
441
  sessionMetadataStore: injectedSessionMetadataStore = null,
@@ -817,10 +835,11 @@ export const createOpenChamberSessionService = (dependencies) => {
817
835
  throw new OpenChamberControlError(resolvedDirectory.error, resolvedDirectory.status || 400);
818
836
  }
819
837
 
820
- const worktreeInput = resolveWorktreeInput(payload);
838
+ const pullRequestInput = resolvePullRequestInput(payload);
839
+ const worktreeInput = pullRequestInput ? null : resolveWorktreeInput(payload);
821
840
  let worktree = null;
822
841
  let sessionDirectory = resolvedDirectory.directory;
823
- if (payload?.worktree && !worktreeInput) {
842
+ if (payload?.worktree && !worktreeInput && !pullRequestInput) {
824
843
  throw new OpenChamberControlError('worktree.name is required when worktree is provided', 400);
825
844
  }
826
845
 
@@ -835,7 +854,25 @@ export const createOpenChamberSessionService = (dependencies) => {
835
854
  });
836
855
  }
837
856
 
838
- if (worktreeInput) {
857
+ if (pullRequestInput) {
858
+ // Never a plain branch worktree instead: without the contributor
859
+ // pipeline the request is refused.
860
+ if (!(createChangeRequestWorktree instanceof Function)
861
+ || !(worktreeBootstrapStore?.read instanceof Function)
862
+ || !(worktreeBootstrapStore?.write instanceof Function)) {
863
+ throw new OpenChamberControlError('Worktrees from pull requests are not available here', 501);
864
+ }
865
+ try {
866
+ worktree = await createChangeRequestWorktree({ directory: resolvedDirectory.directory, ...pullRequestInput });
867
+ } catch (error) {
868
+ throw asControlError(error, `Failed to create a worktree from pull request ${pullRequestInput.number}`);
869
+ }
870
+ sessionDirectory = worktree.path;
871
+ await waitForWorktreeBootstrapReady({
872
+ directory: sessionDirectory,
873
+ bootstrapStore: worktreeBootstrapStore,
874
+ });
875
+ } else if (worktreeInput) {
839
876
  if (!(hydrateWorktreeCheckout instanceof Function)
840
877
  || !(worktreeBootstrapStore?.read instanceof Function)
841
878
  || !(worktreeBootstrapStore?.write instanceof Function)) {
@@ -974,8 +1011,6 @@ export const createOpenChamberSessionService = (dependencies) => {
974
1011
  });
975
1012
  }
976
1013
 
977
- await writeMetadata(targetSessionID, { openchamber: { adopted: true } }, directory);
978
-
979
1014
  const baselineAssistantMessageId = await latestCompletedAssistantMessageID({
980
1015
  client,
981
1016
  sessionID: targetSessionID,
@@ -466,7 +466,6 @@ describe('openchamber session routes', () => {
466
466
  expect(response.body.promptDispatched).toBe(false);
467
467
  expect(sessionCreateMock).toHaveBeenCalledWith({
468
468
  location: { directory: '/repo/app' },
469
- metadata: { openchamber: { adopted: true } },
470
469
  title: 'Side task',
471
470
  });
472
471
  });
@@ -884,6 +883,70 @@ describe('openchamber session routes', () => {
884
883
  expect(sessionPromptMock).not.toHaveBeenCalled();
885
884
  });
886
885
 
886
+ it('starts the session in a worktree made from a pull request', async () => {
887
+ const createChangeRequestWorktree = vi.fn(async () => ({
888
+ name: 'feature-fix', branch: 'feature/fix', path: '/repo/worktrees/feature-fix',
889
+ provenance: { kind: 'contributor-fork' },
890
+ }));
891
+ const { app } = createApp({ createChangeRequestWorktree });
892
+ const response = await request(app)
893
+ .post('/api/openchamber/sessions')
894
+ .send({ directory: '/repo/app', pullRequest: 42, worktree: { name: 'review-42' }, prompt: 'Review it', model: 'openai/gpt-5.5' })
895
+ .expect(200);
896
+
897
+ expect(createChangeRequestWorktree).toHaveBeenCalledExactlyOnceWith({ directory: '/repo/app', number: 42, name: 'review-42' });
898
+ expect(createWorktreeMock).not.toHaveBeenCalled();
899
+ expect(getWorktreeBootstrapStatusMock.mock.invocationCallOrder[0])
900
+ .toBeLessThan(sessionCreateMock.mock.invocationCallOrder[0]);
901
+ expect(sessionCreateMock).toHaveBeenCalledWith(expect.objectContaining({
902
+ location: { directory: '/repo/worktrees/feature-fix' },
903
+ }));
904
+ expect(response.body.directory).toBe('/repo/worktrees/feature-fix');
905
+ expect(response.body.worktree.branch).toBe('feature/fix');
906
+ });
907
+
908
+ it('refuses a pull request it cannot make a worktree for and creates no session', async () => {
909
+ const notFound = Object.assign(new Error('Pull request #42 was not found in acme/app'), { status: 404 });
910
+ const { app } = createApp({ createChangeRequestWorktree: vi.fn(async () => { throw notFound; }) });
911
+
912
+ await request(app)
913
+ .post('/api/openchamber/sessions')
914
+ .send({ directory: '/repo/app', pullRequest: 42, prompt: 'Review it' })
915
+ .expect(404, { error: 'Pull request #42 was not found in acme/app' });
916
+
917
+ expect(sessionCreateMock).not.toHaveBeenCalled();
918
+ expect(getWorktreeBootstrapStatusMock).not.toHaveBeenCalled();
919
+ });
920
+
921
+ it('refuses a pull request where contributor worktrees are not served, with no plain worktree instead', async () => {
922
+ const { app } = createApp();
923
+
924
+ await request(app)
925
+ .post('/api/openchamber/sessions')
926
+ .send({ directory: '/repo/app', pullRequest: 42 })
927
+ .expect(501, { error: 'Worktrees from pull requests are not available here' });
928
+
929
+ expect(createWorktreeMock).not.toHaveBeenCalled();
930
+ expect(sessionCreateMock).not.toHaveBeenCalled();
931
+ });
932
+
933
+ it('rejects a malformed pull request number or a start ref beside it', async () => {
934
+ const createChangeRequestWorktree = vi.fn();
935
+ const { app } = createApp({ createChangeRequestWorktree });
936
+
937
+ await request(app)
938
+ .post('/api/openchamber/sessions')
939
+ .send({ directory: '/repo/app', pullRequest: '#42' })
940
+ .expect(400, { error: 'pullRequest must be a pull or merge request number, such as 42' });
941
+ await request(app)
942
+ .post('/api/openchamber/sessions')
943
+ .send({ directory: '/repo/app', pullRequest: 42, worktree: { startRef: 'main' } })
944
+ .expect(400, { error: 'pullRequest checks out the pull request\'s own branch; do not combine it with startRef or setUpstream' });
945
+
946
+ expect(createChangeRequestWorktree).not.toHaveBeenCalled();
947
+ expect(sessionCreateMock).not.toHaveBeenCalled();
948
+ });
949
+
887
950
  it('does not treat a missing bootstrap record as ready', async () => {
888
951
  getWorktreeBootstrapStatusMock
889
952
  .mockResolvedValueOnce(null)
@@ -1080,7 +1143,7 @@ describe('openchamber session routes', () => {
1080
1143
  .send({ directory: '/repo/app', prompt: 'Carry on', model: 'openai/gpt-5.5', agent: 'build' })
1081
1144
  .expect(200);
1082
1145
 
1083
- expect(sessionMetadataStore.entries.get('ses_fork')).toEqual({ openchamber: { adopted: true, assist: { recap: 'kept' } } });
1146
+ expect(sessionMetadataStore.entries.get('ses_fork')).toEqual({ openchamber: { assist: { recap: 'kept' } } });
1084
1147
  });
1085
1148
 
1086
1149
  it('rejects send and fork requests without a prompt before calling OpenCode', async () => {
@@ -21,6 +21,7 @@
21
21
  * so nothing is lost if a migration turns out wrong.
22
22
  */
23
23
 
24
+ import { asNonEmptyString } from '../shared/guards.js';
24
25
  import fsDefault from 'node:fs';
25
26
  import pathDefault from 'node:path';
26
27
 
@@ -28,12 +29,6 @@ import { createOpenCodeClient as createOpenCodeClientDefault } from './opencode-
28
29
 
29
30
  const LEGACY_FILE_NAME = 'sessions-metadata.json';
30
31
 
31
- const asNonEmptyString = (value) => {
32
- if (typeof value !== 'string') return null;
33
- const trimmed = value.trim();
34
- return trimmed.length > 0 ? trimmed : null;
35
- };
36
-
37
32
  const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
38
33
 
39
34
  /**
@@ -11,7 +11,7 @@ This module provides OpenCode server integration utilities for the web server ru
11
11
  - `packages/web/server/lib/opencode/cli-entry-runtime.js`: CLI entrypoint runtime that detects direct execution, parses CLI options, and starts server bootstrap.
12
12
  - `packages/web/server/lib/opencode/routes.js`: OpenCode/provider settings and auth-related route registration.
13
13
  - `packages/web/server/lib/opencode/v1-migration-topup.js`: re-arms OpenCode's own V1 -> V2 session import for V1 sessions changed by 1.x after the last completed import; runs only before a managed spawn. See "v1-migration-topup.js" below.
14
- - `packages/web/server/lib/opencode/lifecycle.js`: OpenCode process lifecycle runtime (startup, restart, readiness, health monitoring). After readiness it warms the last-used directory only (first entry of the `getWarmupDirectories` dep, best-effort) because OpenCode initializes each directory lazily on first request and that cost would otherwise be paid by the user's first interactive session open. It warms no other projects: on OpenCode 2 the first directory-scoped read boots that location's whole MCP fleet. The warmed directory is also `getDefaultOpenCodeDirectory()`, the scope for server-side reads that have no directory of their own (integrations in `auth.js`, the small model's model and provider lists, the agent check after a restart): v2 answers a location read without one for its own working directory, the user's home for a managed OpenCode, and would start an MCP fleet there. The getter revalidates that directory on every read: once it is no longer a readable directory (deleted, moved, replaced by a file, or stripped of read/execute permission) it is never sent, and the lifecycle advances to another directory the warm pass validated or re-reads `getWarmupDirectories` (coalesced, with a short cooldown) so a currently-valid `lastDirectory` replaces the stale one. A stale cached path would otherwise make every directory-scoped read answer `500 FileSystem.realPath ... ENOENT` — the failure that took the quota credential read down with it.
14
+ - `packages/web/server/lib/opencode/lifecycle.js`: OpenCode process lifecycle runtime (startup, restart, readiness, health monitoring). After readiness it warms the last-used directory only (first entry of the `getWarmupDirectories` dep, best-effort) because OpenCode initializes each directory lazily on first request and that cost would otherwise be paid by the user's first interactive session open. It warms no other projects: on OpenCode 2 the first directory-scoped read boots that location's whole MCP fleet. The warmed directory is also `getDefaultOpenCodeDirectory()`, the scope for server-side reads that have no directory of their own (integrations in `auth.js`, the small model's model and provider lists, the agent check after a restart): v2 answers a location read without one for its own working directory, the user's home for a managed OpenCode, and would start an MCP fleet there. While no warmed directory is usable (no project open), the getter answers the `noProjectDirectory` dep instead, the managed chats root, which the UI also works in then. The getter revalidates that directory on every read: once it is no longer a readable directory (deleted, moved, replaced by a file, or stripped of read/execute permission) it is never sent, and the lifecycle advances to another directory the warm pass validated or re-reads `getWarmupDirectories` (coalesced, with a short cooldown) so a currently-valid `lastDirectory` replaces the stale one. A stale cached path would otherwise make every directory-scoped read answer `500 FileSystem.realPath ... ENOENT` — the failure that took the quota credential read down with it.
15
15
  - The managed OpenCode's environment also carries the user's variables from Settings and from `opencode service set env` (`getUserEnvironment`, see `../environment`), over the inherited environment and under everything OpenChamber sets for OpenCode itself; enterprise mode passes none. `getManagedOpenCodeProcessEnv()` therefore includes them, so a provider key given this way is visible to the credential reader like any other variable OpenCode reports.
16
16
  - `packages/web/server/lib/opencode/provider-env-aliases.js`: mirrors known provider credential env aliases into the managed OpenCode process environment (for example `GEMINI_API_KEY` → `GOOGLE_GENERATIVE_AI_API_KEY`) so OpenCode connection detection and the upstream AI SDK agree on the same key names. Canonical implementation shared by web lifecycle and the VS Code managed spawn path (`packages/vscode/src/provider-env-aliases.ts` re-exports this module).
17
17
  - `packages/web/server/lib/opencode/env-runtime.js`: OpenCode CLI/binary resolution and shell environment runtime.
@@ -59,12 +59,12 @@ unless an output schema is declared; and a tool call no longer receives
59
59
  and OpenChamber resolves the directory itself.
60
60
  - `packages/web/server/lib/opencode/server-utils-runtime.js`: shared server runtime utilities for OpenCode proxy wiring, OpenCode port/readiness helpers, and snapshot fetchers.
61
61
  - `packages/web/server/lib/opencode/openchamber-routes.js`: OpenChamber update and models metadata route registration.
62
- - `packages/web/server/lib/opencode/model-discovery.js`: bounded custom-provider `/models` discovery and optional models.dev enrichment. It accepts only http(s), does not follow redirects, filters transport headers, never returns credentials, and treats models.dev failure as unenriched success. Edit requests may name the provider so the host can use its OpenCode-stored key when the form leaves the key blank, but only while the form's base URL still matches the saved one, so a changed URL never receives the stored credential; an explicitly entered replacement key wins.
62
+ - `packages/web/server/lib/opencode/model-discovery.js`: bounded custom-provider `/models` discovery and optional models.dev enrichment. It accepts only http(s), does not follow redirects, filters transport headers, never returns credentials, and treats models.dev failure as unenriched success. Edit requests may name the provider so the host can fall back to a stored credential when the form leaves the key blank — the key from OpenCode's credential store or, failing that, the provider's stored config entry (`settings.apiKey`, else the first `env` variable name that is set in the server environment) — but only while the form's base URL still matches the saved one, so a changed URL never receives the stored credential; an explicitly entered replacement key wins.
63
63
  - `packages/web/server/lib/opencode/pwa-manifest-routes.js`: PWA manifest route registration with recent-session shortcut resolution and short-lived caching.
64
64
  - `packages/web/server/lib/opencode/project-icon-routes.js`: project icon upload/read/discovery route registration and icon storage orchestration.
65
65
  - `packages/web/server/lib/opencode/skill-routes.js`: route registration for skill config CRUD, supporting files, and skills catalog scan/install flows.
66
66
  - `packages/web/server/lib/opencode/settings-runtime.js`: Settings persistence runtime (disk IO, migrations, normalization, project validation, and persisted update serialization).
67
- - `packages/web/server/lib/opencode/settings-helpers.js`: Settings payload sanitization/format helpers runtime for response shaping and persisted merge prep.
67
+ - `packages/web/server/lib/opencode/settings-helpers.js`: Settings payload sanitization/format helpers runtime for response shaping and persisted merge prep. `lastDirectory: ''` is kept as written: the UI sends it when the last project is removed, and every reader (the warm-up, the legacy last-directory-to-project migration, request directory resolution) treats it as unset.
68
68
  - `packages/web/server/lib/opencode/settings-normalization-runtime.js`: path/settings/tunnel normalization and sanitization helpers runtime used by settings/routes/config wiring.
69
69
  - `packages/web/server/lib/opencode/theme-runtime.js`: custom theme JSON validation and theme directory loading runtime for settings utility routes.
70
70
 
@@ -138,7 +138,10 @@ exists in OpenCode's store.
138
138
  - `readOpenCodeCredentials()`: each integration's active credential, keyed by
139
139
  integration id (the provider id for providers), in the legacy `auth.json`
140
140
  entry shape (`{ type: 'api', key }` /
141
- `{ type: 'oauth', access, refresh, expires, accountId?, enterpriseUrl? }`).
141
+ `{ type: 'oauth', access, refresh, expires, accountId?, enterpriseUrl?, server?, orgID? }`).
142
+ The OAuth projection keeps only `server` and `orgID` from OpenCode Console
143
+ credential metadata, so quota providers can identify a Console sign-in
144
+ without carrying account names or emails.
142
145
  - `getProviderAuth(providerId)`: that map's entry for one provider, or null.
143
146
  - `projectCredentialEntries(entries)`: the wire-to-legacy projection.
144
147
  - `projectEnvironmentKeys(integrations, environment)`: variable values for the env connections OpenCode reports, by integration id.
@@ -214,7 +217,7 @@ Hard rules, verified against v2.0.8 (the completion stamp against v2.0.16)
214
217
  - `OPENCODE_CONFIG_DIR`, `AGENT_DIR`, `COMMAND_DIR`, `SKILL_DIR`, `CONFIG_FILE`: Path constants rooted at `$XDG_CONFIG_HOME/opencode` when `XDG_CONFIG_HOME` is non-empty, otherwise `~/.config/opencode`. These constants are evaluated when the module loads; no files are migrated. `OPENCODE_CONFIG` remains a separate explicit config-file path and is resolved at call time for the custom config layer; it does not replace the global config directory.
215
218
  - `AGENT_SCOPE`, `COMMAND_SCOPE`, `SKILL_SCOPE`: Scope constants with USER and PROJECT values.
216
219
  - `ensureDirs()`: Creates required OpenCode directories.
217
- - `parseMdFile(filePath)`, `writeMdFile(filePath, frontmatter, body)`: Markdown file operations with YAML frontmatter.
220
+ - `parseMdFile(filePath)`, `parseMdFileAsync(filePath)`, `writeMdFile(filePath, frontmatter, body)`: Markdown file operations with YAML frontmatter.
218
221
  - `getConfigPaths(workingDirectory)`, `readConfigLayers(workingDirectory)`, `readConfig(workingDirectory)`: Config file operations with layer merging (user, project, custom). `readConfigLayers` isolates `INVALID_JSONC` per layer: a broken file is omitted from the merge (`{}` for that layer only), recorded on `layerErrors`, and does not block valid sibling layers. Writes still refuse to overwrite the broken file.
219
222
  - `readConfigFile(filePath)`: Reads one config file. Missing, whitespace-only, and comment-only files return `{}`; a comment-only file is recognized by `ValueExpected` being the only parse error. A `jsonc-parser` error that produces a partial or non-object tree throws `INVALID_JSONC` — partial parse trees must never be treated as authoritative (avoids rewriting a `$schema`-only stub over a full config). Content that yields no JSON value for any other reason (YAML, plain text) also throws instead of reading as empty.
220
223
  - `readConfigLayer(filePath)`: Same parse as `readConfigFile`, but isolates `INVALID_JSONC` to `{ config: {}, error }` so plugin/MCP/agent readers can skip one broken layer without aborting valid siblings. Writes still refuse to overwrite the broken file.
@@ -223,10 +226,10 @@ Hard rules, verified against v2.0.8 (the completion stamp against v2.0.16)
223
226
  - `getJsonWriteTarget(layers, preferredScope)`: Determines write target for config updates. Throws `INVALID_JSONC` when the chosen target file is the unparseable layer.
224
227
  - `getAncestors(startDir, stopDir)`, `findWorktreeRoot(startDir)`: Git worktree helpers.
225
228
  - `isPromptFileReference(value)`, `resolvePromptFilePath(reference)`, `writePromptFile(filePath, content)`: Prompt file reference handling.
226
- - `walkSkillMdFiles(rootDir)`: Recursively finds all SKILL.md files.
227
- - `addSkillFromMdFile(skillsMap, skillMdPath, scope, source)`: Parses and indexes a skill file.
229
+ - `walkSkillMdFiles(rootDir)`: Async. Recursively finds all SKILL.md files at any depth, following links and skipping a link back to its own ancestor. Sibling directories are read in parallel, at most 8 file-system calls at once, and the result keeps depth-first readdir order: discovery lets a later file win a duplicate name, so the order is part of the contract.
230
+ - `addSkillFromMdFile(skillsMap, skillMdPath, parsed, scope, source)`: Indexes an already parsed skill file; `parsed` null (unreadable file) adds nothing.
228
231
  - `resolveSkillSearchDirectories(workingDirectory)`: Returns skill search path order (config, project, home, custom).
229
- - `listSkillSupportingFiles(skillDir)`, `readSkillSupportingFile(skillDir, relativePath)`, `writeSkillSupportingFile(skillDir, relativePath, content)`, `deleteSkillSupportingFile(skillDir, relativePath)`: Skill supporting file management.
232
+ - `listSkillSupportingFiles(skillDir)` (async), `readSkillSupportingFile(skillDir, relativePath)`, `writeSkillSupportingFile(skillDir, relativePath, content)`, `deleteSkillSupportingFile(skillDir, relativePath)`: Skill supporting file management.
230
233
 
231
234
  ## Public exports (routes.js)
232
235
  - `registerOpenCodeRoutes(app, dependencies)`: Registers OpenCode-owned HTTP routes and internal module runtime:
@@ -349,6 +352,14 @@ OpenChamber tool injection. Managed launch env strips AppImage `ARGV0` before
349
352
  spawn so zsh-backed OpenCode tools do not rewrite child argv[0] to the AppImage
350
353
  path (#2588).
351
354
 
355
+ Everything the lifecycle and `auth-state-runtime.js` set for OpenCode goes
356
+ through `assignInjectedEnv` (`../injected-env.js`), which records the names of
357
+ the variables it changed in `OPENCHAMBER_INJECTED_ENV`. The agent's shell
358
+ inherits that record, so `openchamber startup enable` run there leaves the
359
+ managed config and password out of the service while the user's own
360
+ `OPENCODE_*` exports carry over (#4604). A value the environment already had is
361
+ not recorded: a user-provided password passed through stays the user's.
362
+
352
363
  Before spawn, `applyProviderEnvAliases` fills unset Google credential aliases
353
364
  from any present sibling (`GOOGLE_GENERATIVE_AI_API_KEY`, `GOOGLE_API_KEY`,
354
365
  `GEMINI_API_KEY`) so a shell that only exports `GEMINI_API_KEY` still satisfies
@@ -882,6 +893,9 @@ within a ten-minute overall deadline.
882
893
  - Skills config CRUD and metadata under `/api/config/skills*`
883
894
  - Skill rename via `PATCH /api/config/skills/:name` with `{ renameTo }` (directory rename preserves `SKILL.md` body and supporting files; restricted to managed skill roots under `.opencode/skills|skill`, `.claude/skills`, and `.agents/skills`)
884
895
  - Skill list responses include authoritative `renamable` derived from the same managed-root policy used by rename
896
+ - `GET /api/config/skills` leaves `sources.md.supportingFiles` out: listing every file of every skill cost seconds when a skill bundles a `.venv` or `node_modules`, and no list consumer reads it. `GET /api/config/skills/:name` still returns it
897
+ - Discovery and skill reads are async (`fs.promises`), and the list enriches one skill at a time, so a large skills tree no longer stalls other requests on the server's event loop
898
+ - Create, edit and rename run one at a time per process, so an existence check cannot go stale before its own write; create also writes `SKILL.md` with `wx` and reports an existing file as "already exists" instead of overwriting it
885
899
  - `disableModelInvocation` (detail `sources.md`, create/update body) is "run only when called": it writes both `disable-model-invocation: true` and `metadata.opencode/autoinvoke: false` so every supported OpenCode 2.x and Claude Code honour it, reads back the way OpenCode resolves the pair (`opencode/autoinvoke` wins), and clearing it removes both keys while keeping other `metadata`. The VS Code runtime mirrors this in `opencodeConfig.ts`
886
900
  - Skills catalog listing/source pagination, scan, and install routes
887
901
  - Supporting skill file read/write/delete routes
@@ -539,7 +539,69 @@ function deleteAgent(agentName, workingDirectory, scope) {
539
539
  throw new Error(`Agent ${agentName} is built-in or not deletable`);
540
540
  }
541
541
 
542
+ // ============== DISABLED ==============
543
+
544
+ /** Agent ids under one agents directory, nested ones as `group/name`. */
545
+ function collectAgentIds(dir, prefix, ids) {
546
+ let entries;
547
+ try {
548
+ entries = fs.readdirSync(dir, { withFileTypes: true });
549
+ } catch {
550
+ return;
551
+ }
552
+ for (const entry of entries) {
553
+ if (entry.isDirectory()) {
554
+ collectAgentIds(path.join(dir, entry.name), `${prefix}${entry.name}/`, ids);
555
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
556
+ ids.add(`${prefix}${entry.name.slice(0, -3)}`);
557
+ }
558
+ }
559
+ }
560
+
561
+ /**
562
+ * Agents switched off with `disabled: true`. OpenCode drops them from its
563
+ * agent list, so Settings learns about them here to offer turning them back
564
+ * on. Every agent defined in a file or a config section is read the way the
565
+ * editor reads it and kept when its own config says disabled.
566
+ */
567
+ function listDisabledAgents(workingDirectory) {
568
+ const lookupCache = createAgentLookupCache();
569
+ const names = new Set();
570
+
571
+ buildUserAgentIndex(lookupCache);
572
+ for (const name of lookupCache.userAgentIndexByName.keys()) names.add(name);
573
+
574
+ if (workingDirectory) {
575
+ const worktreeRoot = findWorktreeRoot(workingDirectory) || path.resolve(workingDirectory);
576
+ for (const base of getAncestors(workingDirectory, worktreeRoot)) {
577
+ for (const dirName of PROJECT_AGENT_DIR_NAMES) {
578
+ collectAgentIds(path.join(base, '.opencode', dirName), '', names);
579
+ }
580
+ }
581
+ }
582
+
583
+ const { mergedConfig } = readConfigLayers(workingDirectory);
584
+ for (const sectionKey of ['agents', 'agent', 'mode']) {
585
+ const section = mergedConfig?.[sectionKey];
586
+ if (isRecord(section)) for (const name of Object.keys(section)) names.add(name);
587
+ }
588
+
589
+ const disabled = [];
590
+ for (const name of [...names].sort((a, b) => a.localeCompare(b))) {
591
+ const entry = getAgentConfig(name, workingDirectory, lookupCache);
592
+ if (entry.config.disabled !== true) continue;
593
+ disabled.push({
594
+ name,
595
+ scope: entry.scope,
596
+ path: entry.path,
597
+ ...(typeof entry.config.description === 'string' ? { description: entry.config.description } : {}),
598
+ });
599
+ }
600
+ return disabled;
601
+ }
602
+
542
603
  export {
604
+ listDisabledAgents,
543
605
  getAgentSources,
544
606
  getAgentConfig,
545
607
  getAgentPermissions,
@@ -1,3 +1,5 @@
1
+ import { assignInjectedEnv } from '../injected-env.js';
2
+
1
3
  export const createOpenCodeAuthStateRuntime = (dependencies) => {
2
4
  const {
3
5
  crypto,
@@ -39,7 +41,7 @@ export const createOpenCodeAuthStateRuntime = (dependencies) => {
39
41
 
40
42
  setAuthPassword(normalized);
41
43
  setAuthSource(source);
42
- process.env.OPENCODE_SERVER_PASSWORD = normalized;
44
+ assignInjectedEnv(process.env, { OPENCODE_SERVER_PASSWORD: normalized });
43
45
  syncToHmrState();
44
46
  return normalized;
45
47
  };
@@ -2,7 +2,7 @@ import type { CredentialEntry, IntegrationInfo, JsonValue } from '@opencode/clie
2
2
 
3
3
  export type LegacyAuthEntry =
4
4
  | { type: 'api'; key: string; metadata?: { [key: string]: JsonValue } }
5
- | { type: 'oauth'; access: string; refresh: string; expires: number; accountId?: string; enterpriseUrl?: string };
5
+ | { type: 'oauth'; access: string; refresh: string; expires: number; accountId?: string; enterpriseUrl?: string; server?: string; orgID?: string };
6
6
  export type LegacyAuthFile = Record<string, LegacyAuthEntry>;
7
7
 
8
8
  /** Where credentials come from: the running OpenCode in production, a fixture in tests. */
@@ -93,8 +93,16 @@ const projectCredentialValue = (value) => {
93
93
  const entry = { type: 'oauth', access: value.access, refresh: value.refresh, expires: value.expires };
94
94
  const accountId = metadataString.safeParse(value.metadata?.accountID).data;
95
95
  const enterpriseUrl = metadataString.safeParse(value.metadata?.enterpriseUrl).data;
96
+ // OpenCode Console signs in through the `opencode` integration and records the
97
+ // Console server and selected organization on the credential. Only these two
98
+ // travel on: the rest of the metadata (email, account name) is not needed by
99
+ // any consumer, and account identifiers stay out of the read path.
100
+ const server = metadataString.safeParse(value.metadata?.server).data;
101
+ const orgID = metadataString.safeParse(value.metadata?.orgID).data;
96
102
  if (accountId !== undefined) entry.accountId = accountId;
97
103
  if (enterpriseUrl !== undefined) entry.enterpriseUrl = enterpriseUrl;
104
+ if (server !== undefined) entry.server = server;
105
+ if (orgID !== undefined) entry.orgID = orgID;
98
106
  return entry;
99
107
  };
100
108