@vantran-se/openchamber-web 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (442) hide show
  1. package/README.md +32 -24
  2. package/bin/cli.js +1 -3
  3. package/bin/cli.test.js +40 -16
  4. package/bin/lib/DOCUMENTATION.md +6 -5
  5. package/bin/lib/cli-args.js +6 -1
  6. package/bin/lib/cli-args.test.js +34 -0
  7. package/bin/lib/cli-lifecycle.js +0 -1
  8. package/bin/lib/cli-network.js +8 -2
  9. package/bin/lib/cli-process.js +1 -1
  10. package/bin/lib/cli-settings-accessors.test.js +21 -13
  11. package/bin/lib/cli-startup.js +17 -11
  12. package/bin/lib/cli-startup.test.js +92 -2
  13. package/bin/lib/commands-connect-url.js +8 -1
  14. package/bin/lib/commands-schedule.js +8 -2
  15. package/bin/lib/commands-serve.js +15 -21
  16. package/bin/lib/commands-update.js +12 -24
  17. package/bin/lib/commands-update.test.js +14 -46
  18. package/dist/assets/DiffView-Cv9MSXl1.js +4 -0
  19. package/dist/assets/FilePreviewCommentMenu-D4kLseHj.js +4 -0
  20. package/dist/assets/FilesView-BVoD8I2x.js +14 -0
  21. package/dist/assets/{GitView-DzuzhIPx.js → GitView-Bzb8p-ew.js} +4 -4
  22. package/dist/assets/GuestSurfacePane-Crdbvsxo.js +1 -0
  23. package/dist/assets/InProgressOperationBanner-CwMjNQgk.js +2 -0
  24. package/dist/assets/{JsonTreeView-B_EPRjzy.js → JsonTreeView-DRKHZfsn.js} +1 -1
  25. package/dist/assets/MarkdownImageGallery-DZtqT1Zn.js +1 -0
  26. package/dist/assets/MarkdownRendererImpl-CqG7HCNd.js +10 -0
  27. package/dist/assets/OnboardingScreen-DMIxwYK5.js +1 -0
  28. package/dist/assets/{PatchDiff-6yh1csza.js → PatchDiff-CUcCg1jk.js} +1 -1
  29. package/dist/assets/PierreDiffViewer-CnnR-dnD.js +228 -0
  30. package/dist/assets/PlanView-RCBadcY7.js +5 -0
  31. package/dist/assets/SettingsView-CjBjZFRO.js +30 -0
  32. package/dist/assets/SettingsWindow-DyM7QuhI.js +1 -0
  33. package/dist/assets/SubmoduleDiffSummary-BWIfgMED.js +1 -0
  34. package/dist/assets/ToolOutputDialog-Q0P3FuAi.js +15 -0
  35. package/dist/assets/ToolPartDiffPreview-p7qR9FpU.js +8 -0
  36. package/dist/assets/{VirtualizedFileDiff-BTJVRkyo.js → VirtualizedFileDiff-CFpjL8Ns.js} +3 -3
  37. package/dist/assets/{Virtualizer-xCEYz34p.js → Virtualizer-C0IsclZn.js} +1 -1
  38. package/dist/assets/WalkthroughView-DI31zD02.js +4 -0
  39. package/dist/assets/appThemeRegistry-B9gkFq9i.js +1 -0
  40. package/dist/assets/{computeVirtualFileMetrics-CmPe29DM.js → computeVirtualFileMetrics-C2WM6JC2.js} +2 -2
  41. package/dist/assets/de-YKfk5MmC.js +14 -0
  42. package/dist/assets/es-_gwT0kr-.js +20 -0
  43. package/dist/assets/fileTreeStatus-D438yi8M.js +1 -0
  44. package/dist/assets/fr-DqzvXmIm.js +18 -0
  45. package/dist/assets/index-BLaHKdtx.js +1 -0
  46. package/dist/assets/index-BkMXRrVQ.css +1 -0
  47. package/dist/assets/index-DxNa7xYB.js +65 -0
  48. package/dist/assets/initializeSharedPreferences-Bck5EAWP.js +1 -0
  49. package/dist/assets/{isDiffPlainText-CQmDCnEv.js → isDiffPlainText-Dgbw8VKk.js} +1 -1
  50. package/dist/assets/{isFilePlainText-CYBUzW4I.js → isFilePlainText-_ZS5UYRj.js} +1 -1
  51. package/dist/assets/ja-B-GFYhQE.js +20 -0
  52. package/dist/assets/ko-mzksfqDC.js +20 -0
  53. package/dist/assets/main-C5Xx8dn_.js +2 -0
  54. package/dist/assets/main-CfBSQM1H.js +990 -0
  55. package/dist/assets/markdownCore-BmWwW1ll.js +7 -0
  56. package/dist/assets/miniChat-mnKwy4vQ.js +2 -0
  57. package/dist/assets/mobile-DeEQd0wZ.js +2 -0
  58. package/dist/assets/nl-CWQr6NSq.js +20 -0
  59. package/dist/assets/{parseDiffFromFile-Cy0PNxun.js → parseDiffFromFile-BUgn-T9p.js} +2 -2
  60. package/dist/assets/patchFileDiff-Dt8LhgBh.js +13 -0
  61. package/dist/assets/pl-UmRcDUX2.js +20 -0
  62. package/dist/assets/pt-BR-BSLybvVF.js +20 -0
  63. package/dist/assets/relocateRestoredSession-B5E9xb3T.js +1 -0
  64. package/dist/assets/renderElectronMiniChatApp-C0a9A5_J.js +2 -0
  65. package/dist/assets/renderMobileApp-BNncg3Pk.js +25 -0
  66. package/dist/assets/runtimeEndpointReset-CZ5aafpl.js +62 -0
  67. package/dist/assets/{selectionMarkdown-TkYk5h9l.js → selectionMarkdown-CUN3wUld.js} +9 -9
  68. package/dist/assets/{sessionOwnership-Bs1xbRfB.js → sessionOwnership-BSuxCEhb.js} +1 -1
  69. package/dist/assets/shikiHighlight-DHDx-6eM.js +1 -0
  70. package/dist/assets/space-apply-DM_2pRW7.js +1 -0
  71. package/dist/assets/space-close-Bpomt8z0.js +1 -0
  72. package/dist/assets/{surface-B6n7wJtt.js → surface-C-jQaGnL.js} +1 -1
  73. package/dist/assets/{textMateThemeFromAppTheme-CDlKffWz.js → textMateThemeFromAppTheme-DlsTphbY.js} +1 -1
  74. package/dist/assets/tr-gaTrX_HK.js +11 -0
  75. package/dist/assets/uk-CnJF6ykl.js +20 -0
  76. package/dist/assets/{useAppFontEffects-DCqCVGlT.js → useAppFontEffects-DGlvOqP_.js} +194 -233
  77. package/dist/assets/useInlineCommentController-kMN4Wg_D.js +2 -0
  78. package/dist/assets/{useRootScrollLock-BowwS8kq.js → useRootScrollLock-DMPpvpNP.js} +2 -2
  79. package/dist/assets/{vendor-base-ui-DX96QhTP.js → vendor-base-ui-Det7U81h.js} +1 -1
  80. package/dist/assets/{vendor-codemirror-autocomplete-DW1FQz9_.js → vendor-codemirror-autocomplete-BSHQHbKu.js} +1 -1
  81. package/dist/assets/{vendor-codemirror-commands-DpmhalwN.js → vendor-codemirror-commands-C4G_dPWe.js} +1 -1
  82. package/dist/assets/{vendor-codemirror-lang-angular-BqExbN1M.js → vendor-codemirror-lang-angular-B6LbBGHB.js} +1 -1
  83. package/dist/assets/{vendor-codemirror-lang-cpp-DZA36HfS.js → vendor-codemirror-lang-cpp-BWWXBx7W.js} +1 -1
  84. package/dist/assets/{vendor-codemirror-lang-css-B07blnnZ.js → vendor-codemirror-lang-css-Cayfb29q.js} +1 -1
  85. package/dist/assets/{vendor-codemirror-lang-go-BM8K9D1-.js → vendor-codemirror-lang-go-BoKlCNKw.js} +1 -1
  86. package/dist/assets/{vendor-codemirror-lang-html-DaH9adfE.js → vendor-codemirror-lang-html-BQWerHsJ.js} +1 -1
  87. package/dist/assets/{vendor-codemirror-lang-java-CsdC0viz.js → vendor-codemirror-lang-java-DWX7Fq_K.js} +1 -1
  88. package/dist/assets/{vendor-codemirror-lang-javascript-CpGYXGuo.js → vendor-codemirror-lang-javascript-DbpEcZM0.js} +2 -2
  89. package/dist/assets/{vendor-codemirror-lang-jinja-BBeGzXw5.js → vendor-codemirror-lang-jinja-D1JIJRA7.js} +1 -1
  90. package/dist/assets/{vendor-codemirror-lang-json-58T70Hqs.js → vendor-codemirror-lang-json-BzgckzmB.js} +1 -1
  91. package/dist/assets/{vendor-codemirror-lang-less-BI-xCA59.js → vendor-codemirror-lang-less-B9EpDXFi.js} +1 -1
  92. package/dist/assets/{vendor-codemirror-lang-liquid-zEcMa-zy.js → vendor-codemirror-lang-liquid-9U7kRbSf.js} +1 -1
  93. package/dist/assets/{vendor-codemirror-lang-markdown-BLKnlLP4.js → vendor-codemirror-lang-markdown-D2TEBIvZ.js} +1 -1
  94. package/dist/assets/{vendor-codemirror-lang-php-9f35M83-.js → vendor-codemirror-lang-php-BT6sDVzE.js} +1 -1
  95. package/dist/assets/vendor-codemirror-lang-python-jAMtYiA9.js +3 -0
  96. package/dist/assets/{vendor-codemirror-lang-rust-MNDL_jZQ.js → vendor-codemirror-lang-rust-CVO56EiL.js} +1 -1
  97. package/dist/assets/{vendor-codemirror-lang-sass-p9epsSNm.js → vendor-codemirror-lang-sass-DZlOoUWu.js} +1 -1
  98. package/dist/assets/{vendor-codemirror-lang-sql-Bqzh0EXX.js → vendor-codemirror-lang-sql-fZ6t1DPw.js} +1 -1
  99. package/dist/assets/{vendor-codemirror-lang-vue-BnNUWV-p.js → vendor-codemirror-lang-vue-DxbqrLiE.js} +1 -1
  100. package/dist/assets/{vendor-codemirror-lang-wast-C2a-oLmk.js → vendor-codemirror-lang-wast-DyU1Gt-I.js} +1 -1
  101. package/dist/assets/vendor-codemirror-lang-xml-C9a_K5_p.js +1 -0
  102. package/dist/assets/{vendor-codemirror-lang-yaml-C0sjswB0.js → vendor-codemirror-lang-yaml-Cptq8l0e.js} +1 -1
  103. package/dist/assets/{vendor-codemirror-language-ByBn5zIW.js → vendor-codemirror-language-DufXoQvE.js} +2 -2
  104. package/dist/assets/vendor-codemirror-language-data-CtwSWKtX.js +2 -0
  105. package/dist/assets/{vendor-codemirror-search-ib-sHD5W.js → vendor-codemirror-search-CvXM_pZg.js} +1 -1
  106. package/dist/assets/{vendor-codemirror-state-G3UuR-be.js → vendor-codemirror-state-Dek9sMtw.js} +1 -1
  107. package/dist/assets/{vendor-codemirror-view-Bniqp7p5.js → vendor-codemirror-view-1e0FQ6Wn.js} +1 -1
  108. package/dist/assets/vendor-framer-motion-DxDVhOLc.js +9 -0
  109. package/dist/assets/vendor-katex-CuQvj9-L.js +257 -0
  110. package/dist/assets/vendor-katex-DdZNUVNG.css +1 -0
  111. package/dist/assets/{vendor-motion-dom-DcCgeG69.js → vendor-motion-dom-HXPOMxb7.js} +1 -1
  112. package/dist/assets/{vendor-opencode-client-DHzhmLwV.js → vendor-opencode-client-BmFcUkbQ.js} +1 -1
  113. package/dist/assets/{vendor-replit-codemirror-vim-DkzqxN2H.js → vendor-replit-codemirror-vim-BlJRn-cW.js} +2 -2
  114. package/dist/assets/{vendor-tanstack-react-virtual-H_JmXOFV.js → vendor-tanstack-react-virtual-CvuLsk7D.js} +1 -1
  115. package/dist/assets/{vendor-tanstack-virtual-core-Dxc8hSTB.js → vendor-tanstack-virtual-core-DPaNcJcT.js} +1 -1
  116. package/dist/assets/zh-CN-t2vigSB1.js +20 -0
  117. package/dist/assets/zh-TW-jAgqBokB.js +20 -0
  118. package/dist/index.html +6 -6
  119. package/dist/mini-chat.html +6 -6
  120. package/dist/mobile.html +6 -6
  121. package/package.json +8 -5
  122. package/server/index.js +80 -36
  123. package/server/lib/agent-memory/actions.js +1 -6
  124. package/server/lib/agent-memory/routes.js +1 -2
  125. package/server/lib/agent-memory/runtime.js +1 -8
  126. package/server/lib/agent-tool/DOCUMENTATION.md +7 -3
  127. package/server/lib/agent-tool/runtime.js +3 -6
  128. package/server/lib/agent-tool/runtime.test.js +0 -2
  129. package/server/lib/browser-control/DOCUMENTATION.md +6 -0
  130. package/server/lib/changelog/update-notes.js +1 -1
  131. package/server/lib/context-obligatory/runtime.js +1 -2
  132. package/server/lib/dev-tunnel/DOCUMENTATION.md +8 -0
  133. package/server/lib/dev-tunnel/client.js +21 -10
  134. package/server/lib/dev-tunnel/tunnel.test.js +34 -0
  135. package/server/lib/dispatch-results/runtime.js +1 -1
  136. package/server/lib/enterprise-mode.d.ts +1 -0
  137. package/server/lib/enterprise-mode.js +13 -2
  138. package/server/lib/enterprise-mode.test.js +21 -0
  139. package/server/lib/event-stream/DOCUMENTATION.md +2 -2
  140. package/server/lib/event-stream/index.js +1 -3
  141. package/server/lib/event-stream/translate-v2.js +1 -9
  142. package/server/lib/event-stream/translate-v2.test.js +0 -11
  143. package/server/lib/fs/DOCUMENTATION.md +5 -2
  144. package/server/lib/fs/routes.js +62 -15
  145. package/server/lib/fs/routes.test.js +278 -48
  146. package/server/lib/fs/workspace-file-names.js +104 -0
  147. package/server/lib/fs/workspace-file-names.test.js +62 -0
  148. package/server/lib/git/DOCUMENTATION.md +17 -5
  149. package/server/lib/git/contributor-provenance-storage.js +9 -6
  150. package/server/lib/git/contributor-provenance-storage.test.js +29 -0
  151. package/server/lib/git/credential-broker.js +1 -1
  152. package/server/lib/git/credential-resolver.js +1 -4
  153. package/server/lib/git/credentials.js +1 -0
  154. package/server/lib/git/discovery-endpoint.js +1 -1
  155. package/server/lib/git/identity-provisioning.js +2 -1
  156. package/server/lib/git/identity-storage.js +3 -4
  157. package/server/lib/git/lfs-discovery.js +14 -14
  158. package/server/lib/git/lfs-discovery.test.js +16 -6
  159. package/server/lib/git/network-operation-plan.js +50 -62
  160. package/server/lib/git/network-operation-plan.test.js +103 -1
  161. package/server/lib/git/network-operation-registry.js +4 -6
  162. package/server/lib/git/network-operation-storage.js +4 -6
  163. package/server/lib/git/network-operations.js +12 -4
  164. package/server/lib/git/network-operations.test.js +19 -6
  165. package/server/lib/git/redaction.js +2 -1
  166. package/server/lib/git/repository-credential-runtime.js +5 -3
  167. package/server/lib/git/repository-credential-runtime.test.js +22 -0
  168. package/server/lib/git/routes.js +31 -137
  169. package/server/lib/git/routes.test.js +2 -2
  170. package/server/lib/git/service.js +143 -27
  171. package/server/lib/git/service.test.js +215 -1
  172. package/server/lib/git/ssh-credential-storage.js +1 -4
  173. package/server/lib/git/submodule-discovery.js +1 -1
  174. package/server/lib/git/worktree-bootstrap-storage.js +3 -4
  175. package/server/lib/git/worktree-bootstrap-storage.test.js +15 -0
  176. package/server/lib/git/worktree-creation.js +194 -0
  177. package/server/lib/git/worktree-creation.test.js +155 -0
  178. package/server/lib/github/DOCUMENTATION.md +11 -6
  179. package/server/lib/github/auth.js +2 -4
  180. package/server/lib/github/device-flow.js +2 -2
  181. package/server/lib/github/gh-cli-credential.js +1 -0
  182. package/server/lib/github/octokit.js +31 -0
  183. package/server/lib/github/pr-status.js +35 -18
  184. package/server/lib/github/pr-status.test.js +271 -3
  185. package/server/lib/github/reference-search.js +102 -13
  186. package/server/lib/github/reference-search.test.js +48 -7
  187. package/server/lib/github/routes.auth.test.js +18 -0
  188. package/server/lib/github/routes.js +360 -6
  189. package/server/lib/github/routes.mutations.test.js +239 -0
  190. package/server/lib/github/routes.test.js +134 -2
  191. package/server/lib/gitlab/DOCUMENTATION.md +5 -1
  192. package/server/lib/gitlab/auth-storage.test.js +1 -0
  193. package/server/lib/gitlab/mappers.js +30 -1
  194. package/server/lib/gitlab/resources.js +230 -6
  195. package/server/lib/gitlab/resources.test.js +179 -0
  196. package/server/lib/gitlab/routes.js +200 -6
  197. package/server/lib/gitlab/routes.test.js +186 -2
  198. package/server/lib/gitlab/validation.js +1 -5
  199. package/server/lib/guests/DOCUMENTATION.md +1 -1
  200. package/server/lib/guests/oauth.js +2 -2
  201. package/server/lib/guests/service.js +1 -1
  202. package/server/lib/guests/sockets.js +1 -1
  203. package/server/lib/injected-env.d.ts +8 -0
  204. package/server/lib/injected-env.js +50 -0
  205. package/server/lib/injected-env.test.js +56 -0
  206. package/server/lib/linear/DOCUMENTATION.md +5 -6
  207. package/server/lib/linear/issues.js +62 -4
  208. package/server/lib/linear/issues.test.js +68 -0
  209. package/server/lib/linear/oauth.js +5 -5
  210. package/server/lib/linear/parse.js +2 -9
  211. package/server/lib/message-queue/DOCUMENTATION.md +23 -1
  212. package/server/lib/message-queue/runtime.js +85 -12
  213. package/server/lib/message-queue/runtime.test.js +172 -2
  214. package/server/lib/notifications/apns-runtime.js +5 -6
  215. package/server/lib/notifications/apns-runtime.test.js +20 -4
  216. package/server/lib/openchamber-control/DOCUMENTATION.md +19 -1
  217. package/server/lib/openchamber-control/actions.js +2 -1
  218. package/server/lib/openchamber-control/service.js +119 -20
  219. package/server/lib/openchamber-control/service.test.js +166 -0
  220. package/server/lib/openchamber-sessions/archive-store.js +1 -6
  221. package/server/lib/openchamber-sessions/routes.js +47 -12
  222. package/server/lib/openchamber-sessions/routes.test.js +65 -2
  223. package/server/lib/openchamber-sessions/session-metadata-store.js +1 -6
  224. package/server/lib/opencode/DOCUMENTATION.md +22 -8
  225. package/server/lib/opencode/agents.js +62 -0
  226. package/server/lib/opencode/auth-state-runtime.js +3 -1
  227. package/server/lib/opencode/auth.d.ts +1 -1
  228. package/server/lib/opencode/auth.js +8 -0
  229. package/server/lib/opencode/auth.test.js +31 -0
  230. package/server/lib/opencode/config-entity-routes.js +16 -0
  231. package/server/lib/opencode/config-entity-v2.test.js +16 -1
  232. package/server/lib/opencode/config-paths.test.js +2 -2
  233. package/server/lib/opencode/config-v2.js +2 -4
  234. package/server/lib/opencode/feature-routes-runtime.js +89 -49
  235. package/server/lib/opencode/lifecycle.js +41 -21
  236. package/server/lib/opencode/lifecycle.test.js +50 -2
  237. package/server/lib/opencode/model-discovery.js +1 -1
  238. package/server/lib/opencode/model-discovery.test.js +17 -0
  239. package/server/lib/opencode/models-metadata.js +0 -2
  240. package/server/lib/opencode/openchamber-routes.test.js +3 -3
  241. package/server/lib/opencode/plugins.js +6 -9
  242. package/server/lib/opencode/provider-source-route.test.js +101 -3
  243. package/server/lib/opencode/providers-disabled.test.js +59 -0
  244. package/server/lib/opencode/providers.js +34 -0
  245. package/server/lib/opencode/proxy.js +11 -3
  246. package/server/lib/opencode/routes.js +40 -2
  247. package/server/lib/opencode/settings-helpers.js +27 -1
  248. package/server/lib/opencode/settings-helpers.test.js +19 -7
  249. package/server/lib/opencode/settings-registry.json +27 -16
  250. package/server/lib/opencode/settings-runtime.test.js +18 -0
  251. package/server/lib/opencode/shared-service-runtime.js +23 -104
  252. package/server/lib/opencode/shared.js +77 -46
  253. package/server/lib/opencode/shared.test.js +12 -12
  254. package/server/lib/opencode/shutdown-runtime.test.js +2 -3
  255. package/server/lib/opencode/skill-routes.js +123 -38
  256. package/server/lib/opencode/skill-routes.test.js +201 -0
  257. package/server/lib/opencode/skills.js +81 -34
  258. package/server/lib/opencode/skills.test.js +176 -21
  259. package/server/lib/opencode/startup-pipeline-runtime.js +6 -3
  260. package/server/lib/opencode/startup-pipeline-runtime.test.js +2 -1
  261. package/server/lib/opencode/watcher.test.js +0 -54
  262. package/server/lib/outbound-proxy.js +52 -0
  263. package/server/lib/outbound-proxy.test.js +55 -0
  264. package/server/lib/package-manager.js +23 -17
  265. package/server/lib/package-manager.test.js +5 -18
  266. package/server/lib/permission-auto-accept/DOCUMENTATION.md +1 -1
  267. package/server/lib/permission-auto-accept/runtime.js +19 -47
  268. package/server/lib/permission-auto-accept/runtime.test.js +2 -2
  269. package/server/lib/project-context/DOCUMENTATION.md +7 -1
  270. package/server/lib/project-context/routes.js +1 -2
  271. package/server/lib/project-context/runtime.js +12 -14
  272. package/server/lib/project-context/runtime.test.js +78 -2
  273. package/server/lib/projects/project-config.js +30 -15
  274. package/server/lib/projects/project-config.test.js +30 -0
  275. package/server/lib/projects/project-setup.js +1 -2
  276. package/server/lib/projects/routes.js +1 -2
  277. package/server/lib/quota/DOCUMENTATION.md +35 -7
  278. package/server/lib/quota/index.js +1 -0
  279. package/server/lib/quota/providers/claude/index.js +1 -1
  280. package/server/lib/quota/providers/cline-pass.js +1 -1
  281. package/server/lib/quota/providers/exe-dev.js +1 -1
  282. package/server/lib/quota/providers/google/index.js +1 -1
  283. package/server/lib/quota/providers/hyper.js +1 -1
  284. package/server/lib/quota/providers/kimi.js +116 -36
  285. package/server/lib/quota/providers/kimi.test.js +128 -3
  286. package/server/lib/quota/providers/opencode-go.js +144 -21
  287. package/server/lib/quota/providers/opencode-go.test.js +229 -6
  288. package/server/lib/quota/providers/openrouter.js +1 -1
  289. package/server/lib/quota/providers/zenmux.js +1 -1
  290. package/server/lib/quota/utils/auth.js +1 -0
  291. package/server/lib/quota/utils/transformers.js +2 -6
  292. package/server/lib/relay/DOCUMENTATION.md +18 -3
  293. package/server/lib/relay/e2ee.js +7 -7
  294. package/server/lib/relay/host-client.js +84 -13
  295. package/server/lib/relay/host-client.test.js +201 -1
  296. package/server/lib/relay/identity.js +6 -29
  297. package/server/lib/relay/identity.test.js +18 -28
  298. package/server/lib/relay/key-store.js +141 -0
  299. package/server/lib/relay/key-store.test.js +138 -0
  300. package/server/lib/relay/service.js +8 -8
  301. package/server/lib/relay/service.test.js +10 -5
  302. package/server/lib/relay/signing-key.js +13 -36
  303. package/server/lib/relay/tunnel-codec.js +10 -10
  304. package/server/lib/route-module-exports.test.js +22 -0
  305. package/server/lib/routing/DOCUMENTATION.md +23 -3
  306. package/server/lib/routing/cache.js +87 -0
  307. package/server/lib/routing/defaults.js +11 -0
  308. package/server/lib/routing/history.js +12 -4
  309. package/server/lib/routing/runtime.js +39 -3
  310. package/server/lib/routing/runtime.test.js +81 -2
  311. package/server/lib/routing/store.js +12 -1
  312. package/server/lib/routing/store.test.js +13 -0
  313. package/server/lib/scheduled-tasks/DOCUMENTATION.md +59 -1
  314. package/server/lib/scheduled-tasks/existing-session.js +119 -0
  315. package/server/lib/scheduled-tasks/existing-session.test.js +350 -0
  316. package/server/lib/scheduled-tasks/loops.js +1 -8
  317. package/server/lib/scheduled-tasks/routes.js +8 -8
  318. package/server/lib/scheduled-tasks/runtime.js +103 -13
  319. package/server/lib/scheduled-tasks/runtime.test.js +48 -0
  320. package/server/lib/scheduled-tasks/service.js +33 -11
  321. package/server/lib/scheduled-tasks/service.test.js +128 -2
  322. package/server/lib/scheduled-tasks/session-defaults.js +27 -0
  323. package/server/lib/scheduled-tasks/session-defaults.test.js +19 -0
  324. package/server/lib/session-assist/context.js +1 -0
  325. package/server/lib/session-folders/routes.js +2 -2
  326. package/server/lib/session-goal/objectives.js +1 -1
  327. package/server/lib/session-knowledge/routes.js +2 -2
  328. package/server/lib/session-knowledge/runtime.js +1 -2
  329. package/server/lib/session-work/DOCUMENTATION.md +39 -14
  330. package/server/lib/session-work/questions.js +31 -2
  331. package/server/lib/session-work/runtime.js +22 -13
  332. package/server/lib/session-work/runtime.test.js +43 -26
  333. package/server/lib/session-work/state.js +10 -4
  334. package/server/lib/session-work/state.test.js +8 -1
  335. package/server/lib/shared/DOCUMENTATION.md +89 -0
  336. package/server/lib/shared/guards.js +53 -0
  337. package/server/lib/shared/guards.test.js +112 -0
  338. package/server/lib/skills-catalog/DOCUMENTATION.md +10 -8
  339. package/server/lib/skills-catalog/cache.js +3 -2
  340. package/server/lib/skills-catalog/curated-sources.js +5 -1
  341. package/server/lib/skills-catalog/git.js +107 -20
  342. package/server/lib/skills-catalog/git.test.js +78 -0
  343. package/server/lib/skills-catalog/install.js +21 -12
  344. package/server/lib/skills-catalog/scan.js +25 -15
  345. package/server/lib/skills-catalog/source.js +26 -1
  346. package/server/lib/skills-catalog/source.test.js +27 -0
  347. package/server/lib/source-control/DOCUMENTATION.md +2 -1
  348. package/server/lib/source-control/audit-storage.js +7 -6
  349. package/server/lib/source-control/audit-storage.test.js +11 -0
  350. package/server/lib/source-control/binding-service.js +2 -5
  351. package/server/lib/source-control/change-request-worktree.js +90 -0
  352. package/server/lib/source-control/change-request-worktree.test.js +121 -0
  353. package/server/lib/source-control/mutation-executor.js +1 -1
  354. package/server/lib/source-control/mutation-storage.js +11 -5
  355. package/server/lib/source-control/provider-instance.js +1 -2
  356. package/server/lib/source-control/repository-identity.js +1 -1
  357. package/server/lib/source-control/routes.js +14 -2
  358. package/server/lib/spaces/DOCUMENTATION.md +42 -17
  359. package/server/lib/spaces/command-stream.test.js +15 -0
  360. package/server/lib/spaces/dispatcher.js +7 -0
  361. package/server/lib/spaces/dispatcher.test.js +17 -0
  362. package/server/lib/spaces/hardening.js +2 -2
  363. package/server/lib/spaces/hardening.test.js +1 -1
  364. package/server/lib/spaces/host.js +2 -0
  365. package/server/lib/spaces/journey.js +66 -5
  366. package/server/lib/spaces/journey.test.js +95 -4
  367. package/server/lib/spaces/layout.js +1 -1
  368. package/server/lib/spaces/places/docker-disk.js +38 -14
  369. package/server/lib/spaces/places/docker-disk.test.js +80 -1
  370. package/server/lib/spaces/places/docker.js +35 -11
  371. package/server/lib/spaces/places/fake-docker.js +22 -3
  372. package/server/lib/spaces/places/memory-place.js +3 -0
  373. package/server/lib/spaces/places/packed.docker.live.test.js +1 -1
  374. package/server/lib/spaces/routes.js +5 -0
  375. package/server/lib/spaces/routes.test.js +4 -2
  376. package/server/lib/spaces/run-command.js +9 -2
  377. package/server/lib/spaces/tools.js +4 -5
  378. package/server/lib/spaces/tools.test.js +2 -2
  379. package/server/lib/terminal/runtime.js +18 -6
  380. package/server/lib/terminal/runtime.test.js +66 -0
  381. package/server/lib/tts/index.js +3 -0
  382. package/server/lib/tts/service.js +4 -2
  383. package/server/lib/walkthrough/DOCUMENTATION.md +3 -1
  384. package/server/lib/walkthrough/index.js +2 -3
  385. package/server/lib/walkthrough/pr-source.test.js +22 -0
  386. package/server/lib/walkthrough/sources.js +6 -1
  387. package/server/opencode-proxy.test.js +127 -0
  388. package/bin/lib/commands-startup.test.js +0 -15
  389. package/dist/assets/CodeMirrorCommentWidgets-CVDSjcrP.js +0 -1
  390. package/dist/assets/DiffView-Ct7vL3fD.js +0 -4
  391. package/dist/assets/FilesView-DT0uEYgT.js +0 -17
  392. package/dist/assets/GuestSurfacePane-DVvk1VPu.js +0 -1
  393. package/dist/assets/LinearIssuesView-C10MwA9h.js +0 -1
  394. package/dist/assets/MarkdownImageGallery-BbN_K_3d.js +0 -1
  395. package/dist/assets/MarkdownRendererImpl-BLt2Tkk9.js +0 -10
  396. package/dist/assets/OnboardingScreen-Bgp5A3WB.js +0 -1
  397. package/dist/assets/PierreDiffViewer-CWJLj-S-.js +0 -210
  398. package/dist/assets/PlanView-DsRWnsOI.js +0 -5
  399. package/dist/assets/PublishDialog-CCeKloRu.js +0 -2
  400. package/dist/assets/SettingsView-BuPPsNPp.js +0 -30
  401. package/dist/assets/SettingsWindow-CM5fL8Ot.js +0 -1
  402. package/dist/assets/SubmoduleDiffSummary-CfsMcfiQ.js +0 -1
  403. package/dist/assets/ToolOutputDialog-mZrfSofz.js +0 -15
  404. package/dist/assets/ToolPartDiffPreview-BUGCRYEL.js +0 -8
  405. package/dist/assets/WalkthroughView-ClBo7PPd.js +0 -4
  406. package/dist/assets/appThemeRegistry-CxzMxR-H.js +0 -1
  407. package/dist/assets/de-CBla4hjd.js +0 -14
  408. package/dist/assets/es-BmnZtxTD.js +0 -20
  409. package/dist/assets/fileTreeStatus-BSHFIGEg.js +0 -1
  410. package/dist/assets/fr-OVKIBUZO.js +0 -18
  411. package/dist/assets/index-CKCFKmjF.js +0 -1
  412. package/dist/assets/index-Dbx4V4GN.css +0 -1
  413. package/dist/assets/index-vQ1Pg1cr.js +0 -61
  414. package/dist/assets/ja-6v-yr-gE.js +0 -20
  415. package/dist/assets/ko-I_ZLvgQ9.js +0 -20
  416. package/dist/assets/main-CmO4dRtR.js +0 -2
  417. package/dist/assets/main-b9vqhlYM.js +0 -999
  418. package/dist/assets/markdownCore-BPEZRQfP.js +0 -7
  419. package/dist/assets/miniChat-B5Oi7B-6.js +0 -2
  420. package/dist/assets/mobile-CVtShif9.js +0 -2
  421. package/dist/assets/nl-DcvIJf46.js +0 -20
  422. package/dist/assets/patchFileDiff-CW3wCH1O.js +0 -13
  423. package/dist/assets/pl-JxCUl0sd.js +0 -20
  424. package/dist/assets/pt-BR-L89mjja2.js +0 -20
  425. package/dist/assets/relocateRestoredSession-DYh_9oBa.js +0 -1
  426. package/dist/assets/renderElectronMiniChatApp-BD-1JHSg.js +0 -2
  427. package/dist/assets/renderMobileApp-7s5InXt1.js +0 -25
  428. package/dist/assets/runtimeEndpointReset-DqLJC6L1.js +0 -53
  429. package/dist/assets/shikiHighlight-CXgZnvHh.js +0 -1
  430. package/dist/assets/space-apply-1trGVIl8.js +0 -1
  431. package/dist/assets/space-close-y7j-NuBv.js +0 -1
  432. package/dist/assets/tr-ka5J1flt.js +0 -11
  433. package/dist/assets/uk-PyGe6Q_u.js +0 -20
  434. package/dist/assets/useInlineCommentController-CrjSGCpi.js +0 -2
  435. package/dist/assets/vendor-codemirror-lang-python-B7ClrMIT.js +0 -3
  436. package/dist/assets/vendor-codemirror-lang-xml-C8tGnS64.js +0 -1
  437. package/dist/assets/vendor-codemirror-language-data-q2bKO1EM.js +0 -2
  438. package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +0 -1
  439. package/dist/assets/vendor-katex-CHD9BdDO.js +0 -257
  440. package/dist/assets/vendor-katex-Cp579UoJ.css +0 -1
  441. package/dist/assets/zh-CN-B0P2tybJ.js +0 -20
  442. package/dist/assets/zh-TW-Bs9C5cN8.js +0 -20
@@ -1,3 +1,4 @@
1
+ import { isRecord } from '../shared/guards.js';
1
2
  import fs from 'fs';
2
3
  import path from 'path';
3
4
  import {
@@ -57,10 +58,6 @@ function validatePluginSpec(spec) {
57
58
  return spec.trim();
58
59
  }
59
60
 
60
- function isRecord(value) {
61
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
62
- }
63
-
64
61
  function hasOptions(options) {
65
62
  return isRecord(options) && Object.keys(options).length > 0;
66
63
  }
@@ -249,7 +246,8 @@ function decodePluginId(id) {
249
246
  }
250
247
 
251
248
  /** Accept a v1 string/tuple or a v2 `{package, options}` object. */
252
- function parsePluginRaw(raw) {
249
+ /** @public Exposed to tests that dynamically import the plugins module. */
250
+ export function parsePluginRaw(raw) {
253
251
  const entity = toPluginEntity(raw);
254
252
  if (!entity) {
255
253
  throw codedError('Plugin spec must be a string, [string, object], or {package, options}', 'INVALID_SPEC');
@@ -260,7 +258,8 @@ function parsePluginRaw(raw) {
260
258
  }
261
259
 
262
260
  /** Always v2: a bare string, or `{package, options}`. Never a tuple. */
263
- function serializePluginEntry(entry) {
261
+ /** @public Exposed to tests that dynamically import the plugins module. */
262
+ export function serializePluginEntry(entry) {
264
263
  const spec = validatePluginSpec(entry?.spec);
265
264
  const serialized = fromPluginEntity({ package: spec, options: hasOptions(entry?.options) ? entry.options : undefined });
266
265
  if (serialized === null) {
@@ -447,7 +446,5 @@ export {
447
446
  writePluginDirFile,
448
447
  deletePluginDirFile,
449
448
  encodePluginId,
450
- decodePluginId,
451
- parsePluginRaw,
452
- serializePluginEntry,
449
+ decodePluginId
453
450
  };
@@ -1,4 +1,7 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
2
5
  import express from 'express';
3
6
  import request from 'supertest';
4
7
  import { registerOpenCodeRoutes } from './routes.js';
@@ -165,9 +168,10 @@ describe('POST /api/provider/discover-models', () => {
165
168
  getProviderSources: vi.fn(),
166
169
  });
167
170
 
168
- // The route's auth module reads the real OpenCode credential store. This
169
- // fixture cannot inject that private module, so the module-level fallback
170
- // is covered in model-discovery.test.js and this keeps the route contract:
171
+ // With no credential source configured the route's credential-store read
172
+ // fails and discovery falls back to the key in the form; the stored-config
173
+ // fallback has its own describe below, and env resolution in stored keys
174
+ // is covered in model-discovery.test.js. This keeps the route contract:
171
175
  // provider id enters, no credential leaves in the response.
172
176
  const response = await request(app)
173
177
  .post('/api/provider/discover-models')
@@ -181,6 +185,100 @@ describe('POST /api/provider/discover-models', () => {
181
185
  });
182
186
  });
183
187
 
188
+ describe('POST /api/provider/discover-models stored-config fallback', () => {
189
+ // The route reads the stored entry from the merged config layers. Point
190
+ // OPENCODE_CONFIG (resolved on every read) at a temp fixture so the entry
191
+ // is exactly what a provider form save writes: the v2 `providers` shape.
192
+ let root;
193
+ let previousOpenCodeConfig;
194
+
195
+ const serveModels = () => {
196
+ const requests = [];
197
+ globalThis.fetch = vi.fn(async (_url, init) => {
198
+ requests.push(init.headers.Authorization);
199
+ return new Response(JSON.stringify({ data: [{ id: 'model-a' }] }), { status: 200 });
200
+ });
201
+ return requests;
202
+ };
203
+
204
+ const writeStoredProvider = (providerID, entry) => {
205
+ const configPath = path.join(root, 'opencode.json');
206
+ fs.writeFileSync(configPath, JSON.stringify({ providers: { [providerID]: entry } }), 'utf8');
207
+ process.env.OPENCODE_CONFIG = configPath;
208
+ };
209
+
210
+ const discover = (body) => {
211
+ const app = express();
212
+ app.use(express.json());
213
+ registerOpenCodeRoutes(app, {
214
+ resolveProjectDirectory: vi.fn(async () => ({ directory: '/projects/app' })),
215
+ getProviderSources: vi.fn(),
216
+ });
217
+ return request(app).post('/api/provider/discover-models').send(body);
218
+ };
219
+
220
+ beforeEach(() => {
221
+ // The credential store is reachable but empty: the fallback must come
222
+ // from the stored config entry.
223
+ configureOpenCodeCredentials({ list: async () => [] });
224
+ root = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-discovery-'));
225
+ previousOpenCodeConfig = process.env.OPENCODE_CONFIG;
226
+ });
227
+
228
+ afterEach(() => {
229
+ configureOpenCodeCredentials(null);
230
+ if (previousOpenCodeConfig === undefined) delete process.env.OPENCODE_CONFIG;
231
+ else process.env.OPENCODE_CONFIG = previousOpenCodeConfig;
232
+ fs.rmSync(root, { recursive: true, force: true });
233
+ });
234
+
235
+ it('sends the first env variable that is set, not just the first name', async () => {
236
+ const providerID = `env-order-${Date.now()}`;
237
+ process.env.OPENCHAMBER_DISCOVERY_SECOND = 'second-secret';
238
+ delete process.env.OPENCHAMBER_DISCOVERY_FIRST;
239
+ writeStoredProvider(providerID, {
240
+ package: 'aisdk:@ai-sdk/openai-compatible',
241
+ env: ['OPENCHAMBER_DISCOVERY_FIRST', 'OPENCHAMBER_DISCOVERY_SECOND'],
242
+ settings: { baseURL: 'https://stored.test/v1' },
243
+ });
244
+ const requests = serveModels();
245
+ try {
246
+ const response = await discover({ providerID, baseURL: 'https://stored.test/v1', enrich: false }).expect(200);
247
+ expect(requests).toEqual(['Bearer second-secret']);
248
+ expect(JSON.stringify(response.body)).not.toContain('second-secret');
249
+ } finally {
250
+ delete process.env.OPENCHAMBER_DISCOVERY_SECOND;
251
+ }
252
+ });
253
+
254
+ it('falls back to settings.apiKey when the credential store has none', async () => {
255
+ const providerID = `settings-key-${Date.now()}`;
256
+ writeStoredProvider(providerID, { settings: { baseURL: 'https://stored.test/v1', apiKey: 'config-secret' } });
257
+ const requests = serveModels();
258
+ const response = await discover({ providerID, baseURL: 'https://stored.test/v1', enrich: false }).expect(200);
259
+ expect(requests).toEqual(['Bearer config-secret']);
260
+ expect(JSON.stringify(response.body)).not.toContain('config-secret');
261
+ });
262
+
263
+ it('prefers the credential store key and keeps the stored base URL guard', async () => {
264
+ const providerID = `store-wins-${Date.now()}`;
265
+ configureOpenCodeCredentials({
266
+ list: async () => [{ integrationID: providerID, active: true, value: { type: 'key', key: 'store-secret' } }],
267
+ });
268
+ writeStoredProvider(providerID, { settings: { baseURL: 'https://stored.test/v1', apiKey: 'config-secret' } });
269
+ const requests = serveModels();
270
+ // Before the v2 config read, storedBaseURL was always undefined and the
271
+ // endpoint guard discarded even the credential-store key.
272
+ await discover({ providerID, baseURL: 'https://stored.test/v1', enrich: false }).expect(200);
273
+ expect(requests).toEqual(['Bearer store-secret']);
274
+
275
+ // A form base URL that moved away from the stored one receives no key.
276
+ requests.length = 0;
277
+ await discover({ providerID, baseURL: 'https://moved.test/v1', enrich: false }).expect(200);
278
+ expect(requests).toEqual([undefined]);
279
+ });
280
+ });
281
+
184
282
  describe('Git initialization through the proxy', () => {
185
283
  it('refuses home, a disk root, and a request without a directory', async () => {
186
284
  const agent = request(createApp(vi.fn()));
@@ -0,0 +1,59 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'vitest';
2
+ import { spawnSync } from 'child_process';
3
+ import fs from 'fs';
4
+ import os from 'os';
5
+ import path from 'path';
6
+ import { fileURLToPath } from 'url';
7
+
8
+ const providersModule = fileURLToPath(new URL('./providers.js', import.meta.url));
9
+
10
+ // The user config path is fixed when shared.js loads, so the calls run in a
11
+ // child process pointed at a temporary OPENCODE_CONFIG_DIR; the real user
12
+ // config is never touched.
13
+ const run = (configDir, body) => {
14
+ const script = `
15
+ import { readDisabledProviders, setProviderDisabled } from ${JSON.stringify(providersModule)};
16
+ ${body}
17
+ `;
18
+ const result = spawnSync(process.execPath, ['--input-type=module', '-e', script], {
19
+ env: { ...process.env, OPENCODE_CONFIG_DIR: configDir },
20
+ encoding: 'utf8',
21
+ });
22
+ if (result.status !== 0) throw new Error(result.stderr);
23
+ return JSON.parse(result.stdout.trim().split('\n').pop());
24
+ };
25
+
26
+ describe('disabled providers', () => {
27
+ let configDir;
28
+ beforeEach(() => {
29
+ configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-disabled-providers-'));
30
+ });
31
+ afterEach(() => {
32
+ fs.rmSync(configDir, { recursive: true, force: true });
33
+ });
34
+
35
+ test('disables and re-enables a provider in the user config, keeping other keys', () => {
36
+ fs.writeFileSync(path.join(configDir, 'opencode.json'), JSON.stringify({ model: 'a/b', disabled_providers: ['groq'] }));
37
+
38
+ const afterDisable = run(configDir, `
39
+ setProviderDisabled('openrouter', true, null);
40
+ setProviderDisabled('openrouter', true, null);
41
+ console.log(JSON.stringify(readDisabledProviders(null)));
42
+ `);
43
+ expect(afterDisable).toEqual(['groq', 'openrouter']);
44
+
45
+ const afterEnable = run(configDir, `
46
+ setProviderDisabled('groq', false, null);
47
+ setProviderDisabled('openrouter', false, null);
48
+ console.log(JSON.stringify(readDisabledProviders(null)));
49
+ `);
50
+ expect(afterEnable).toEqual([]);
51
+ const written = JSON.parse(fs.readFileSync(path.join(configDir, 'opencode.json'), 'utf8'));
52
+ expect(written.model).toBe('a/b');
53
+ expect(written.disabled_providers).toBeUndefined();
54
+ });
55
+
56
+ test('refuses an invalid provider id', () => {
57
+ expect(() => run(configDir, `setProviderDisabled('Bad Id', true, null); console.log('[]');`)).toThrow('Invalid provider ID');
58
+ });
59
+ });
@@ -314,8 +314,42 @@ function removeProviderConfig(providerId, workingDirectory, scope = 'user') {
314
314
  return true;
315
315
  }
316
316
 
317
+ /**
318
+ * Providers turned off in the user's OpenCode config (`disabled_providers`).
319
+ * OpenCode 2 drops them from its provider list, so this list is the only
320
+ * place Settings can still show them to turn them back on.
321
+ */
322
+ function readDisabledProviders(workingDirectory) {
323
+ const layers = readConfigLayers(workingDirectory);
324
+ const config = getConfigForPath(layers, layers.paths.userPath);
325
+ return Array.isArray(config.disabled_providers)
326
+ ? config.disabled_providers.filter((entry) => typeof entry === 'string' && entry.length > 0)
327
+ : [];
328
+ }
329
+
330
+ function setProviderDisabled(providerId, disabled, workingDirectory) {
331
+ if (typeof providerId !== 'string' || !PROVIDER_ID_PATTERN.test(providerId)) {
332
+ const error = new Error('Invalid provider ID');
333
+ error.statusCode = 400;
334
+ throw error;
335
+ }
336
+ const layers = readConfigLayers(workingDirectory);
337
+ const targetPath = layers.paths.userPath;
338
+ const config = getConfigForPath(layers, targetPath);
339
+ const current = Array.isArray(config.disabled_providers)
340
+ ? config.disabled_providers.filter((entry) => entry !== providerId)
341
+ : [];
342
+ const next = disabled ? [...current, providerId] : current;
343
+ if (next.length > 0) config.disabled_providers = next;
344
+ else delete config.disabled_providers;
345
+ writeConfig(config, targetPath || CONFIG_FILE);
346
+ return next;
347
+ }
348
+
317
349
  export {
318
350
  getProviderSources,
351
+ readDisabledProviders,
352
+ setProviderDisabled,
319
353
  removeProviderConfig,
320
354
  upsertProviderConfig,
321
355
  validateCustomProviderConfig,
@@ -459,9 +459,17 @@ export const registerOpenCodeProxy = (app, deps) => {
459
459
 
460
460
  const replayParsedBody = (proxyReq, req) => {
461
461
  const body = serializeParsedBody(req, proxyReq);
462
- if (!body) return;
463
- // http-proxy copies the incoming headers, so a chunked request would reach
464
- // OpenCode with both framing headers and be rejected as ambiguous.
462
+ if (!body) {
463
+ // DO NOT remove transfer-encoding from a stream without content-length:
464
+ // Node then sends tunneled prompt bodies without framing, and OpenCode
465
+ // reads an empty payload (#4576).
466
+ if (proxyReq.getHeader('content-length') !== undefined) {
467
+ proxyReq.removeHeader('transfer-encoding');
468
+ }
469
+ return;
470
+ }
471
+ // DO NOT keep the copied transfer-encoding alongside the replayed body's
472
+ // content-length: OpenCode rejects requests with both headers (#4280).
465
473
  proxyReq.removeHeader('transfer-encoding');
466
474
  proxyReq.setHeader('content-length', String(body.length));
467
475
  proxyReq.write(body);
@@ -9,7 +9,7 @@ import {
9
9
  import { getClaudeCliAuthStatus } from './claude-cli-auth.js';
10
10
  import { OPENCODE_CONFIG_DIR, readConfigLayers } from './shared.js';
11
11
  import { settingsSurfaceOf } from './settings-files.js';
12
- import { parseWebSearchSelection } from './config-v2.js';
12
+ import { parseWebSearchSelection, readSectionEntry, toProviderEntity } from './config-v2.js';
13
13
  import { getWebSearchSource, setWarmingEnabled, setWebSearchSelection } from './websearch-config.js';
14
14
  import {
15
15
  CREDENTIAL_LIST_ERROR,
@@ -19,6 +19,7 @@ import {
19
19
  isProviderConnectRequest,
20
20
  } from '../enterprise-mode.js';
21
21
  import { discoverProviderModels } from './model-discovery.js';
22
+ import { readDisabledProviders, setProviderDisabled } from './providers.js';
22
23
  import { vcsInitRefusal, vcsInitRefusalBody } from '../git/repository-root.js';
23
24
 
24
25
  export const registerOpenCodeRoutes = (app, dependencies) => {
@@ -268,6 +269,31 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
268
269
  }
269
270
  });
270
271
 
272
+ // Providers turned off in the user's OpenCode config. OpenCode leaves them
273
+ // out of its provider list, so Settings reads them here to offer them back.
274
+ app.get('/api/provider/disabled', (_req, res) => {
275
+ try {
276
+ return res.json({ providers: readDisabledProviders(null) });
277
+ } catch (error) {
278
+ console.error('Failed to read disabled providers:', error);
279
+ return res.status(500).json({ error: 'Failed to read disabled providers' });
280
+ }
281
+ });
282
+
283
+ app.put('/api/provider/:providerId/disabled', async (req, res) => {
284
+ if (typeof req.body?.disabled !== 'boolean') {
285
+ return res.status(400).json({ error: 'disabled must be true or false' });
286
+ }
287
+ try {
288
+ const providers = setProviderDisabled(req.params.providerId, req.body.disabled, null);
289
+ return res.json({ providers, ...buildAppliedResponse('Provider updated.') });
290
+ } catch (error) {
291
+ if (error?.statusCode === 400) return res.status(400).json({ error: error.message });
292
+ console.error('Failed to update disabled providers:', error);
293
+ return res.status(500).json({ error: 'Failed to update disabled providers' });
294
+ }
295
+ });
296
+
271
297
  app.get('/api/provider/:providerId/source', async (req, res) => {
272
298
  try {
273
299
  const { providerId } = req.params;
@@ -403,7 +429,19 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
403
429
  const { getProviderAuth } = await getAuthLibrary();
404
430
  const storedAuth = await getProviderAuth(providerID);
405
431
  storedApiKey = storedAuth?.type === 'api' && typeof storedAuth.key === 'string' ? storedAuth.key : null;
406
- storedBaseURL = readConfigLayers().mergedConfig?.provider?.[providerID]?.options?.baseURL;
432
+ const { value: rawProvider } = readSectionEntry(readConfigLayers().mergedConfig, 'providers', providerID);
433
+ const providerEntity = rawProvider ? toProviderEntity(rawProvider) : null;
434
+ storedBaseURL = providerEntity?.settings?.baseURL;
435
+ if (!storedApiKey) {
436
+ // OpenCode tries each `env` name in order, so pick the first one
437
+ // that is set; when none is, keep the first so discovery can name
438
+ // it in the error instead of failing as an anonymous 401. This
439
+ // reads the server's environment: a variable that exists only in
440
+ // the managed OpenCode launch environment (Settings user env) is
441
+ // not visible here.
442
+ const envName = providerEntity?.env?.find((name) => process.env[name]) ?? providerEntity?.env?.[0];
443
+ storedApiKey = providerEntity?.settings?.apiKey || (envName ? `{env:${envName}}` : null);
444
+ }
407
445
  } catch {
408
446
  storedApiKey = null;
409
447
  }
@@ -232,7 +232,12 @@ export const createSettingsHelpers = (dependencies) => {
232
232
  if (typeof candidate.splashFgDark === 'string' && candidate.splashFgDark.trim().length > 0) {
233
233
  result.splashFgDark = candidate.splashFgDark.trim();
234
234
  }
235
- if (typeof candidate.lastDirectory === 'string' && candidate.lastDirectory.length > 0) {
235
+ // An empty string forgets the last directory (the last project was
236
+ // removed). Every reader treats it as unset, so nothing is warmed at start
237
+ // or turned back into a project.
238
+ if (candidate.lastDirectory === '') {
239
+ result.lastDirectory = '';
240
+ } else if (typeof candidate.lastDirectory === 'string') {
236
241
  const normalized = normalizePathForPersistence(candidate.lastDirectory);
237
242
  if (typeof normalized === 'string' && normalized.length > 0) {
238
243
  result.lastDirectory = normalized;
@@ -354,6 +359,9 @@ export const createSettingsHelpers = (dependencies) => {
354
359
  if (typeof candidate.sidebarShowRecentSection === 'boolean') {
355
360
  result.sidebarShowRecentSection = candidate.sidebarShowRecentSection;
356
361
  }
362
+ if (typeof candidate.sidebarShowChatsSection === 'boolean') {
363
+ result.sidebarShowChatsSection = candidate.sidebarShowChatsSection;
364
+ }
357
365
 
358
366
  if (Array.isArray(candidate.securityScopedBookmarks)) {
359
367
  result.securityScopedBookmarks = normalizeStringArray(candidate.securityScopedBookmarks);
@@ -389,6 +397,9 @@ export const createSettingsHelpers = (dependencies) => {
389
397
  if (typeof candidate.draftStartersScheduleTaskAdded === 'boolean') {
390
398
  result.draftStartersScheduleTaskAdded = candidate.draftStartersScheduleTaskAdded;
391
399
  }
400
+ if (candidate.addProjectDialogDismissed === true || candidate.addProjectDialogDismissed === false) {
401
+ result.addProjectDialogDismissed = candidate.addProjectDialogDismissed;
402
+ }
392
403
 
393
404
 
394
405
  if (typeof candidate.uiFont === 'string' && candidate.uiFont.length > 0) {
@@ -446,6 +457,9 @@ export const createSettingsHelpers = (dependencies) => {
446
457
  if (typeof candidate.diffWrapLines === 'boolean') {
447
458
  result.diffWrapLines = candidate.diffWrapLines;
448
459
  }
460
+ if (typeof candidate.diffHideWhitespace === 'boolean') {
461
+ result.diffHideWhitespace = candidate.diffHideWhitespace;
462
+ }
449
463
  if (typeof candidate.persistChatDraft === 'boolean') {
450
464
  result.persistChatDraft = candidate.persistChatDraft;
451
465
  }
@@ -470,6 +484,9 @@ export const createSettingsHelpers = (dependencies) => {
470
484
  result.fileEditorKeymap = mode;
471
485
  }
472
486
  }
487
+ if (typeof candidate.fileEditorVimMappings === 'string') {
488
+ result.fileEditorVimMappings = candidate.fileEditorVimMappings.slice(0, 10_000);
489
+ }
473
490
  if (Array.isArray(candidate.providerOrder)) {
474
491
  result.providerOrder = normalizeStringArray(candidate.providerOrder);
475
492
  }
@@ -485,6 +502,12 @@ export const createSettingsHelpers = (dependencies) => {
485
502
  if (typeof candidate.sessionWorkAutoOpen === 'boolean') {
486
503
  result.sessionWorkAutoOpen = candidate.sessionWorkAutoOpen;
487
504
  }
505
+ if (typeof candidate.sessionReviewOfferEnabled === 'boolean') {
506
+ result.sessionReviewOfferEnabled = candidate.sessionReviewOfferEnabled;
507
+ }
508
+ if (typeof candidate.sessionWorkKeepInGroup === 'boolean') {
509
+ result.sessionWorkKeepInGroup = candidate.sessionWorkKeepInGroup;
510
+ }
488
511
  if (typeof candidate.sessionGoalEnabled === 'boolean') {
489
512
  result.sessionGoalEnabled = candidate.sessionGoalEnabled;
490
513
  }
@@ -847,6 +870,9 @@ export const createSettingsHelpers = (dependencies) => {
847
870
  if (Array.isArray(candidate.recentAgents)) {
848
871
  result.recentAgents = normalizeStringArray(candidate.recentAgents);
849
872
  }
873
+ if (Array.isArray(candidate.favoriteAgents)) {
874
+ result.favoriteAgents = normalizeStringArray(candidate.favoriteAgents);
875
+ }
850
876
 
851
877
  const recentEfforts = sanitizeRecentEfforts(candidate.recentEfforts);
852
878
  if (recentEfforts) {
@@ -76,6 +76,15 @@ describe('settings helpers', () => {
76
76
  expect(helpers.sanitizeSettingsUpdate({ gitlabClientId: 42 })).toEqual({});
77
77
  });
78
78
 
79
+ it('forgets the last directory when asked with an empty string', () => {
80
+ // Removing the last project sends this. A kept last directory would be
81
+ // turned back into a project on the next read and warmed at every start.
82
+ const helpers = createTestHelpers();
83
+ expect(helpers.sanitizeSettingsUpdate({ lastDirectory: '' })).toEqual({ lastDirectory: '' });
84
+ expect(helpers.sanitizeSettingsUpdate({ lastDirectory: '/home/testuser/project' })).toEqual({ lastDirectory: '/home/testuser/project' });
85
+ expect(helpers.sanitizeSettingsUpdate({ lastDirectory: 42 })).toEqual({});
86
+ });
87
+
79
88
  it('round-trips section order and preserves it across unrelated writes', () => {
80
89
  const helpers = createTestHelpers();
81
90
  const changes = helpers.sanitizeSettingsUpdate({ workStatusSectionOrder: ['mcp', 'session', 'mcp', null, ''] });
@@ -101,7 +110,7 @@ describe('settings helpers', () => {
101
110
  expect(helpers.formatSettingsResponse(next).workStatusHiddenSectionsExplicit).toBe(true);
102
111
  expect(helpers.sanitizeSettingsUpdate({ workStatusHiddenSectionsExplicit: 'true' }).workStatusHiddenSectionsExplicit).toBeUndefined();
103
112
  });
104
- it('imports from the packed @openchamber/web tarball without escaping the published package', async () => {
113
+ it('imports from the packed @vantran-se/openchamber-web tarball without escaping the published package', async () => {
105
114
  const tempRoot = mkdtempSync(join(tmpdir(), 'settings-helpers-pack-'));
106
115
  const packDir = join(tempRoot, 'pack');
107
116
  const extractDir = join(tempRoot, 'extract');
@@ -658,6 +667,9 @@ describe('settings helpers', () => {
658
667
  expect(helpers.sanitizeSettingsUpdate({ recentAgents: ['build', 'plan'] })).toEqual({
659
668
  recentAgents: ['build', 'plan'],
660
669
  });
670
+ expect(helpers.sanitizeSettingsUpdate({ favoriteAgents: ['build', 'incident'] })).toEqual({
671
+ favoriteAgents: ['build', 'incident'],
672
+ });
661
673
  });
662
674
 
663
675
  it('round-trips recentEfforts as a Record<string, string[]>', () => {
@@ -810,7 +822,7 @@ describe('settings registry gate', () => {
810
822
  lastDirectory: '/home/testuser/project', homeDirectory: '/home/testuser', opencodeBinary: '/usr/local/bin/opencode',
811
823
  projects: [{ id: 'p', path: '/home/testuser/project' }], activeProjectId: 'p',
812
824
  securityScopedBookmarks: ['bookmark'], pinnedDirectories: ['/home/testuser/project'],
813
- desktopLanAccessEnabled: true, desktopKeepAwakeEnabled: true, desktopMinimizeToTrayEnabled: true, desktopMacMenuBarEnabled: true, desktopLinuxNativeFrame: true,
825
+ desktopLanAccessEnabled: true, desktopKeepAwakeEnabled: true, desktopMinimizeToTrayEnabled: true, desktopMacMenuBarEnabled: true, desktopLinuxNativeFrame: true, sidebarShowChatsSection: false,
814
826
  desktopUiPassword: 'secret', githubClientId: 'client', githubScopes: 'repo', gitlabClientId: 'gitlab-client', skillCatalogs: [{ id: 'c', label: 'C', source: 'https://x' }],
815
827
  defaultGitIdentityId: 'global', permissionAutoAccept: { sessions: { s: true }, revision: 1 }, permissionDefaultMode: 'safety', messageSearchEnabled: true, messageSearchReasoningEnabled: true,
816
828
  agentControlToolEnabled: true, agentWebToolEnabled: true, browserProvider: 'builtin', agentMemoryToolEnabled: true, agentNotifyToolEnabled: true, agentToolsCodeMode: true, isolatedSpacesEnabled: true, isolatedSpacesIdleStop: { enabled: true, hours: 4 }, openCodeUpdateToastDismissedVersion: '1.0.0',
@@ -825,19 +837,19 @@ describe('settings registry gate', () => {
825
837
  chatRenderMode: 'live', activityRenderMode: 'summary', mermaidRenderingMode: 'svg', userMessageRenderingMode: 'markdown', collapsibleUserMessages: true,
826
838
  stickyUserHeader: true, promptNavigatorEnabled: true, wideChatLayoutEnabled: true, showSplitAssistantMessageActions: true, showToolFileIcons: true,
827
839
  codeBlockLineWrap: true, tableCellWrap: true, copyMessagesAsPlainText: true, showTurnChangedFiles: true, showExpandedBashTools: true, showExpandedEditTools: true, toolJsonViewMode: 'raw',
828
- timeFormatPreference: '24h', weekStartPreference: 'monday', messageStreamTransport: 'ws', diffLayoutPreference: 'inline', diffWrapLines: true,
840
+ timeFormatPreference: '24h', weekStartPreference: 'monday', messageStreamTransport: 'ws', diffLayoutPreference: 'inline', diffWrapLines: true, diffHideWhitespace: true,
829
841
  gitChangesViewMode: 'tree', gitmojiEnabled: true, defaultFileViewerPreview: true, directoryShowHidden: true, filesViewShowGitignored: true,
830
- fileEditorKeymap: 'vim', autoSaveEnabled: true, autoCreateWorktree: true, sessionTabsEnabled: true,
842
+ fileEditorKeymap: 'vim', fileEditorVimMappings: 'inoremap jk <Esc>', autoSaveEnabled: true, autoCreateWorktree: true, sessionTabsEnabled: true,
831
843
  allowPromptingSubagentSessions: true, inputSpellcheckEnabled: true, enterToSend: true, enterToSendConfigured: true, persistChatDraft: true,
832
844
  largeTextPasteBehavior: 'attach', followUpBehavior: 'steer', queueModeEnabled: true, inputHistoryScope: 'global', inputHistoryLimit: 40,
833
- draftStarters: [{ type: 'command', name: 'plan-feature' }], draftStartersVisible: true, draftStartersCraftGoalAdded: true, draftStartersScheduleTaskAdded: true,
845
+ draftStarters: [{ type: 'command', name: 'plan-feature' }], draftStartersVisible: true, draftStartersCraftGoalAdded: true, draftStartersScheduleTaskAdded: true, addProjectDialogDismissed: true,
834
846
  fontSize: 100, terminalFontSize: 14, editorFontSize: 14, uiFont: 'inter', monoFont: 'jetbrains-mono', customUiFont: 'Maple Mono NF CN', customMonoFont: 'Maple Mono', padding: 100, cornerRadius: 8,
835
847
  shortcutOverrides: { 'chat.send': 'mod+enter' },
836
848
  defaultModel: 'anthropic/claude', defaultVariant: 'high', defaultAgent: 'build', smallModelUseDefault: false, smallModelOverride: 'anthropic/haiku',
837
849
  walkthroughModelOverride: 'anthropic/claude', zenModel: 'zen/model',
838
850
  favoriteModels: [{ providerID: 'anthropic', modelID: 'claude' }], hiddenModels: [{ providerID: 'openai', modelID: 'gpt' }], collapsedModelProviders: ['openai'],
839
- recentModels: [{ providerID: 'anthropic', modelID: 'claude' }], lastSelectedModel: 'anthropic/claude', recentAgents: ['build'], recentEfforts: { 'anthropic/claude': ['high'] }, providerOrder: ['anthropic'],
840
- sessionRecapEnabled: true, sessionSuggestionEnabled: true, sessionWorkEnabled: true, sessionWorkAutoOpen: true, sessionGoalEnabled: true, sessionGoalChecker: 'small-model', sessionGoalMaxAutoTurns: 50, sessionGoalDefaultBudgetEnabled: true, sessionGoalDefaultBudget: 5,
851
+ recentModels: [{ providerID: 'anthropic', modelID: 'claude' }], lastSelectedModel: 'anthropic/claude', recentAgents: ['build'], favoriteAgents: ['build'], recentEfforts: { 'anthropic/claude': ['high'] }, providerOrder: ['anthropic'],
852
+ sessionRecapEnabled: true, sessionSuggestionEnabled: true, sessionWorkEnabled: true, sessionWorkAutoOpen: true, sessionReviewOfferEnabled: true, sessionWorkKeepInGroup: true, sessionGoalEnabled: true, sessionGoalChecker: 'small-model', sessionGoalMaxAutoTurns: 50, sessionGoalDefaultBudgetEnabled: true, sessionGoalDefaultBudget: 5,
841
853
  summarizeLastMessage: true, summaryThreshold: 100, summaryLength: 50, maxLastMessageLength: 200, showDeletionDialog: true,
842
854
  nativeNotificationsEnabled: true, notificationMode: 'always', notifyOnSubtasks: true, notifyOnCompletion: true, notifyOnError: true, notifyOnQuestion: true,
843
855
  notificationTemplates: { completion: { title: 't', message: 'm' } }, showOpenCodeUpdateNotifications: true, reportUsage: true,
@@ -280,6 +280,10 @@
280
280
  "scope": "profile",
281
281
  "perSurface": true
282
282
  },
283
+ "sidebarShowChatsSection": {
284
+ "scope": "profile",
285
+ "perSurface": true
286
+ },
283
287
  "workStatusSectionOrder": {
284
288
  "scope": "profile"
285
289
  },
@@ -377,6 +381,9 @@
377
381
  "diffWrapLines": {
378
382
  "scope": "profile"
379
383
  },
384
+ "diffHideWhitespace": {
385
+ "scope": "profile"
386
+ },
380
387
  "gitChangesViewMode": {
381
388
  "scope": "profile"
382
389
  },
@@ -395,6 +402,9 @@
395
402
  "fileEditorKeymap": {
396
403
  "scope": "profile"
397
404
  },
405
+ "fileEditorVimMappings": {
406
+ "scope": "profile"
407
+ },
398
408
  "autoSaveEnabled": {
399
409
  "scope": "profile"
400
410
  },
@@ -451,6 +461,9 @@
451
461
  "draftStartersScheduleTaskAdded": {
452
462
  "scope": "profile"
453
463
  },
464
+ "addProjectDialogDismissed": {
465
+ "scope": "profile"
466
+ },
454
467
  "fontSize": {
455
468
  "scope": "profile",
456
469
  "perSurface": true
@@ -528,6 +541,9 @@
528
541
  "recentAgents": {
529
542
  "scope": "profile"
530
543
  },
544
+ "favoriteAgents": {
545
+ "scope": "profile"
546
+ },
531
547
  "recentEfforts": {
532
548
  "scope": "profile"
533
549
  },
@@ -551,6 +567,17 @@
551
567
  "mobile"
552
568
  ]
553
569
  },
570
+ "sessionReviewOfferEnabled": {
571
+ "scope": "profile",
572
+ "surfaces": [
573
+ "web",
574
+ "desktop",
575
+ "mobile"
576
+ ]
577
+ },
578
+ "sessionWorkKeepInGroup": {
579
+ "scope": "profile"
580
+ },
554
581
  "sessionGoalEnabled": {
555
582
  "scope": "profile"
556
583
  },
@@ -785,22 +812,6 @@
785
812
  "scope": "device",
786
813
  "local": true
787
814
  },
788
- "linearIssueListStatus": {
789
- "scope": "device",
790
- "local": true
791
- },
792
- "linearIssueListAssignee": {
793
- "scope": "device",
794
- "local": true
795
- },
796
- "linearIssueListTeamIdByRuntime": {
797
- "scope": "device",
798
- "local": true
799
- },
800
- "linearIssueListPriority": {
801
- "scope": "device",
802
- "local": true
803
- },
804
815
  "showTerminalQuickKeysOnDesktop": {
805
816
  "scope": "device",
806
817
  "local": true
@@ -82,6 +82,24 @@ describe('settings runtime', () => {
82
82
  }
83
83
  });
84
84
 
85
+ it('turns a stored last directory into a project only while one is stored', async () => {
86
+ const { runtime, tempRoot, cleanup } = await createRuntime();
87
+ try {
88
+ const directory = path.join(tempRoot, 'removed-project');
89
+ await fsPromises.mkdir(directory);
90
+ // The legacy migration: no projects, a last directory on disk.
91
+ await runtime.writeSettingsToDisk({ projects: [], lastDirectory: directory });
92
+ expect((await runtime.readSettingsFromDiskMigrated()).projects).toEqual([
93
+ expect.objectContaining({ path: directory }),
94
+ ]);
95
+ // Removing the last project forgets the last directory with ''.
96
+ await runtime.writeSettingsToDisk({ projects: [], lastDirectory: '' });
97
+ expect((await runtime.readSettingsFromDiskMigrated()).projects).toEqual([]);
98
+ } finally {
99
+ await cleanup();
100
+ }
101
+ });
102
+
85
103
  it('uses OpenChamber themes when a new install has no theme preferences', async () => {
86
104
  const { runtime, cleanup } = await createRuntime();
87
105
  try {