@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
@@ -67,7 +67,7 @@ describe.skipIf(!PACKED_ENABLED)('development build inside a space: docker (live
67
67
  const installed = lock.packages['node_modules/@openchamber/sdk'];
68
68
  expect(installed.version).toBe(JSON.parse(sdk).version);
69
69
  expect(installed.resolved).toMatch(/^file:.*openchamber-sdk\.tgz$/);
70
- expect(lock.packages['node_modules/@openchamber/web'].resolved).toMatch(/^file:.*openchamber-web\.tgz$/);
70
+ expect(lock.packages['node_modules/@vantran-se/openchamber-web'].resolved).toMatch(/^file:.*openchamber-web\.tgz$/);
71
71
  });
72
72
 
73
73
  it('leaves the tarballs out of the volume', async () => {
@@ -43,6 +43,7 @@ const STATUS_BY_CODE = new Map([
43
43
  ['space_preparing', 409],
44
44
  ['space_creation_failed', 409],
45
45
  ['space_busy', 409],
46
+ ['image_pulling', 409],
46
47
  ['isolated_spaces_off', 404],
47
48
  ['space_not_running', 409],
48
49
  ['branch_exists', 409],
@@ -205,6 +206,10 @@ export function registerSpaceRoutes(app, { getJourney, getPlaces = () => [], rea
205
206
  app.post(`${SPACES_ROUTE}/places/:placeId/clean-up`, withJourney(async (journey, req, res) => {
206
207
  res.json(await journey.cleanUpDisk(String(req.params.placeId ?? '')));
207
208
  }));
209
+ // Downloads the image ahead of the first space; answers the disk at once, the download goes on.
210
+ app.post(`${SPACES_ROUTE}/places/:placeId/image`, withJourney(async (journey, req, res) => {
211
+ res.status(202).json(await journey.pullImage(String(req.params.placeId ?? '')));
212
+ }));
208
213
 
209
214
  app.get(SPACES_ROUTE, withJourney(async (journey, _req, res) => {
210
215
  res.json({ spaces: await journey.listSpaces({ access: true }) });
@@ -68,6 +68,7 @@ const journeyOf = (overrides = {}) => {
68
68
  readSetup: record('readSetup', { setup: null, output: null }),
69
69
  readDisk: record('readDisk', { imageBytes: null, toolsBytes: 0, spacesBytes: 0, freeBytes: 0, freesImage: false }),
70
70
  cleanUpDisk: record('cleanUpDisk', { freedBytes: 0, kept: [], disk: null }),
71
+ pullImage: record('pullImage', { imageBytes: null, imagePulling: true, imageFailure: null, toolsBytes: 0, spacesBytes: 0, freeBytes: 0, freesImage: false }),
71
72
  ...overrides,
72
73
  };
73
74
  };
@@ -75,7 +76,7 @@ const journeyOf = (overrides = {}) => {
75
76
  describe('space routes', () => {
76
77
  it('answers every journey route with 404 and isolated_spaces_off while the feature is off, and the switch still works', async () => {
77
78
  const { call, calls } = await serve({ journey: null, switchState: { enabled: false } });
78
- for (const [method, path] of [['GET', ''], ['POST', ''], ['GET', '/places'], ['POST', `/${ID}/start`], ['POST', `/${ID}/stop`], ['POST', `/${ID}/restart`], ['POST', `/${ID}/restart-opencode`], ['POST', `/${ID}/grants`], ['DELETE', `/${ID}`], ['GET', `/${ID}/journal`], ['GET', `/${ID}/apply`], ['POST', `/${ID}/apply`], ['GET', '/idle-stop'], ['PUT', '/idle-stop'], ['POST', `/${ID}/setup`], ['GET', `/${ID}/setup`], ['GET', '/places/docker/disk'], ['POST', '/places/docker/clean-up']]) {
79
+ for (const [method, path] of [['GET', ''], ['POST', ''], ['GET', '/places'], ['POST', `/${ID}/start`], ['POST', `/${ID}/stop`], ['POST', `/${ID}/restart`], ['POST', `/${ID}/restart-opencode`], ['POST', `/${ID}/grants`], ['DELETE', `/${ID}`], ['GET', `/${ID}/journal`], ['GET', `/${ID}/apply`], ['POST', `/${ID}/apply`], ['GET', '/idle-stop'], ['PUT', '/idle-stop'], ['POST', `/${ID}/setup`], ['GET', `/${ID}/setup`], ['GET', '/places/docker/disk'], ['POST', '/places/docker/clean-up'], ['POST', '/places/docker/image']]) {
79
80
  expect(await call(method, `${SPACES_ROUTE}${path}`, method === 'GET' || method === 'DELETE' ? undefined : {}), `${method} ${path}`).toEqual({ status: 404, body: { code: 'isolated_spaces_off', message: 'Isolated spaces are turned off.', details: null } });
80
81
  }
81
82
  expect(await call('GET', `${SPACES_ROUTE}/switch`)).toEqual({ status: 200, body: { enabled: false, spaces: [] } });
@@ -107,6 +108,7 @@ describe('space routes', () => {
107
108
  expect(await call('GET', SPACES_ROUTE)).toEqual({ status: 200, body: { spaces: [{ id: ID, name: 'One', state: 'running' }] } });
108
109
  expect(await call('GET', `${SPACES_ROUTE}/places/docker/disk`)).toEqual({ status: 200, body: { imageBytes: null, toolsBytes: 0, spacesBytes: 0, freeBytes: 0, freesImage: false } });
109
110
  expect(await call('POST', `${SPACES_ROUTE}/places/docker/clean-up`)).toEqual({ status: 200, body: { freedBytes: 0, kept: [], disk: null } });
111
+ expect(await call('POST', `${SPACES_ROUTE}/places/docker/image`)).toEqual({ status: 202, body: { imageBytes: null, imagePulling: true, imageFailure: null, toolsBytes: 0, spacesBytes: 0, freeBytes: 0, freesImage: false } });
110
112
  const request = { projectDirectory: '/home/me/project', name: 'One', start: 'clean', network: { mode: 'open' } };
111
113
  expect(await call('POST', SPACES_ROUTE, request)).toEqual({ status: 202, body: { id: ID, state: 'preparing' } });
112
114
  expect(await call('POST', `${SPACES_ROUTE}/${ID}/start`)).toEqual({ status: 200, body: { id: ID, state: 'running' } });
@@ -122,7 +124,7 @@ describe('space routes', () => {
122
124
  expect(await call('GET', `${SPACES_ROUTE}/${ID}/setup`)).toEqual({ status: 200, body: { setup: null, output: null } });
123
125
  expect(await call('DELETE', `${SPACES_ROUTE}/${ID}`)).toEqual({ status: 200, body: { id: ID, removed: true } });
124
126
  expect(journey.calls).toEqual([
125
- ['listSpaces', { access: true }], ['readDisk', 'docker'], ['cleanUpDisk', 'docker'], ['createSpace', request], ['startSpace', ID], ['stopSpace', ID], ['restartSpace', ID], ['restartOpenCode', ID], ['grantAccess', ID, { kind: 'domain', upstream: 'https://registry.example.com/' }], ['openDomain', ID, { domain: 'registry.npmjs.org' }], ['readJournal', ID], ['previewApply', ID], ['applySpace', ID, { as: 'branch', branch: 'b' }], ['runSetup', ID, { commands: ['npm ci'] }], ['readSetup', ID], ['removeSpace', ID, { allowUnsaved: false }],
127
+ ['listSpaces', { access: true }], ['readDisk', 'docker'], ['cleanUpDisk', 'docker'], ['pullImage', 'docker'], ['createSpace', request], ['startSpace', ID], ['stopSpace', ID], ['restartSpace', ID], ['restartOpenCode', ID], ['grantAccess', ID, { kind: 'domain', upstream: 'https://registry.example.com/' }], ['openDomain', ID, { domain: 'registry.npmjs.org' }], ['readJournal', ID], ['previewApply', ID], ['applySpace', ID, { as: 'branch', branch: 'b' }], ['runSetup', ID, { commands: ['npm ci'] }], ['readSetup', ID], ['removeSpace', ID, { allowUnsaved: false }],
126
128
  ]);
127
129
  });
128
130
 
@@ -475,12 +475,19 @@ class CommandStream extends Duplex {
475
475
  callback(error);
476
476
  }
477
477
 
478
- /** Emits `timeout` after `milliseconds` without a byte in either direction. Zero cancels it. */
478
+ /**
479
+ * Emits `timeout` after `milliseconds` without a byte in either direction. Zero cancels it, and
480
+ * takes the callback off again, as `net.Socket` does: the http client sets a timeout with its
481
+ * listener on every request a pooled stream carries, and cancels it with the same listener.
482
+ */
479
483
  setTimeout(milliseconds, callback) {
480
484
  if (this.#timer) clearTimeout(this.#timer);
481
485
  this.#timer = null;
482
486
  this.timeout = milliseconds;
483
- if (callback) this.once('timeout', callback);
487
+ if (callback) {
488
+ if (milliseconds > 0) this.once('timeout', callback);
489
+ else this.removeListener('timeout', callback);
490
+ }
484
491
  if (milliseconds > 0) {
485
492
  this.#timer = setTimeout(() => { this.#timer = null; this.emit('timeout'); }, milliseconds);
486
493
  this.#timer.unref?.();
@@ -15,7 +15,6 @@ import { FILLER_PROGRAM } from './tools-filler.js';
15
15
  // Where the filler puts the tarballs of a packed source. It is a tmpfs, so they vanish with the filler.
16
16
  export const TOOLS_STAGING_PATH = '/tmp/openchamber-fill';
17
17
 
18
- const WEB_PACKAGE_NAME = '@vantran-se/openchamber-web';
19
18
  const WEB_TARBALL_NAME = 'openchamber-web.tgz';
20
19
  const SDK_TARBALL_NAME = 'openchamber-sdk.tgz';
21
20
 
@@ -57,7 +56,7 @@ const requireRevision = (value) => {
57
56
  const openCodePackages = (version) => ({ '@opencode/cli': version, '@opencode/plugin': version });
58
57
 
59
58
  /**
60
- * The versions a released host installs: its own web package, and OpenCode at the
59
+ * The versions a released host installs: its own `@vantran-se/openchamber-web`, and OpenCode at the
61
60
  * version of the `@opencode/client` this server was built against.
62
61
  */
63
62
  export function readHostToolVersions(packageJsonUrl = new URL('../../../package.json', import.meta.url)) {
@@ -68,14 +67,14 @@ export function readHostToolVersions(packageJsonUrl = new URL('../../../package.
68
67
  throw new SpaceError('tools_versions_unreadable', `Could not read the package.json of the OpenChamber server: ${error.message}`);
69
68
  }
70
69
  return {
71
- webVersion: requireVersion(manifest.version, WEB_PACKAGE_NAME),
70
+ webVersion: requireVersion(manifest.version, '@vantran-se/openchamber-web'),
72
71
  openCodeVersion: requireVersion(manifest.dependencies?.['@opencode/client'], 'OpenCode'),
73
72
  };
74
73
  }
75
74
 
76
75
  export function createRegistryToolsSource({ webVersion, openCodeVersion, revision = '' }) {
77
76
  const packages = {
78
- [WEB_PACKAGE_NAME]: requireVersion(webVersion, WEB_PACKAGE_NAME),
77
+ '@vantran-se/openchamber-web': requireVersion(webVersion, '@vantran-se/openchamber-web'),
79
78
  ...openCodePackages(requireVersion(openCodeVersion, 'OpenCode')),
80
79
  };
81
80
  return finishSource({
@@ -111,7 +110,7 @@ export function createPackedToolsSource({ webTarballPath, sdkTarballPath, openCo
111
110
  canonical: { kind: 'packed', tarballs: files.map((file) => ({ name: file.name, sha256: sha256(file.bytes) })), packages, revision: requireRevision(revision) },
112
111
  packageJson: packageJsonText({
113
112
  dependencies: {
114
- '@openchamber/web': `file:${TOOLS_STAGING_PATH}/${WEB_TARBALL_NAME}`,
113
+ '@vantran-se/openchamber-web': `file:${TOOLS_STAGING_PATH}/${WEB_TARBALL_NAME}`,
115
114
  '@openchamber/sdk': `file:${TOOLS_STAGING_PATH}/${SDK_TARBALL_NAME}`,
116
115
  ...packages,
117
116
  },
@@ -44,7 +44,7 @@ describe('readHostToolVersions', () => {
44
44
  });
45
45
 
46
46
  describe('createRegistryToolsSource', () => {
47
- it('asks for the fork package at the exact host version', () => {
47
+ it('asks for exact versions, with the plugin at the OpenCode version', () => {
48
48
  const source = createRegistryToolsSource(VERSIONS);
49
49
 
50
50
  expect(source.description).toBe('web 1.24.2, opencode 1.18.31');
@@ -79,7 +79,7 @@ describe('createPackedToolsSource', () => {
79
79
  name: 'openchamber-space-tools',
80
80
  private: true,
81
81
  dependencies: {
82
- '@openchamber/web': 'file:/tmp/openchamber-fill/openchamber-web.tgz',
82
+ '@vantran-se/openchamber-web': 'file:/tmp/openchamber-fill/openchamber-web.tgz',
83
83
  '@openchamber/sdk': 'file:/tmp/openchamber-fill/openchamber-sdk.tgz',
84
84
  '@opencode/cli': '1.18.31',
85
85
  '@opencode/plugin': '1.18.31',
@@ -95,6 +95,7 @@ export function createTerminalRuntime({
95
95
  isRequestOriginAllowed, rejectWebSocketUpgrade, TERMINAL_INPUT_WS_HEARTBEAT_INTERVAL_MS,
96
96
  loadPtyProvider, terminalTerminationGraceMs = TERMINATION_GRACE_MS, shutdownProcesses = shutdownTerminalProcesses,
97
97
  environmentRuntime = null,
98
+ logger = console,
98
99
  }) {
99
100
  const sessions = new Map();
100
101
  const pendingSessionCreates = new Map();
@@ -407,11 +408,22 @@ export function createTerminalRuntime({
407
408
 
408
409
  const upgradeHandler = (req, socket, head) => {
409
410
  if (parseRequestPathname(req.url) !== TERMINAL_WS_PATH) return;
411
+ const logUpgradeFailure = (error) => {
412
+ logger.warn?.(`[terminal] websocket upgrade failed: ${error?.message || error}`);
413
+ };
410
414
  const accept = () => {
411
415
  if (!wsServer) { rejectWebSocketUpgrade(socket, 500, 'Terminal WebSocket unavailable'); return; }
412
416
  try {
413
417
  wsServer.handleUpgrade(req, socket, head, (ws) => wsServer.emit('connection', ws, req));
414
- } catch { rejectWebSocketUpgrade(socket, 500, 'Upgrade failed'); }
418
+ } catch (error) {
419
+ // handleUpgrade only throws once it has taken the socket: either the
420
+ // handshake already wrote its 101 (a second upgrade of the same socket,
421
+ // or a throw from the connection callback), so the connection is no
422
+ // longer HTTP. Writing a status line here would append an HTTP response
423
+ // to a switched-protocol socket; drop it and log why instead.
424
+ logUpgradeFailure(error);
425
+ socket.destroy();
426
+ }
415
427
  };
416
428
  const checkOrigin = () => {
417
429
  try {
@@ -424,15 +436,15 @@ export function createTerminalRuntime({
424
436
  void result.then((allowed) => {
425
437
  if (allowed) accept();
426
438
  else rejectWebSocketUpgrade(socket, 403, 'Invalid origin');
427
- }).catch(() => rejectWebSocketUpgrade(socket, 500, 'Upgrade failed'));
428
- } catch { rejectWebSocketUpgrade(socket, 500, 'Upgrade failed'); }
439
+ }).catch((error) => { logUpgradeFailure(error); rejectWebSocketUpgrade(socket, 500, 'Upgrade failed'); });
440
+ } catch (error) { logUpgradeFailure(error); rejectWebSocketUpgrade(socket, 500, 'Upgrade failed'); }
429
441
  };
430
442
  if (isOpaqueOriginRequest(req)) { rejectWebSocketUpgrade(socket, 403, 'Invalid origin'); return; }
431
443
  if (!uiAuthController?.enabled) {
432
444
  void isPasswordlessSocketOriginAllowed(req, isRequestOriginAllowed).then((allowed) => {
433
445
  if (allowed) accept();
434
446
  else rejectWebSocketUpgrade(socket, 403, 'Invalid origin');
435
- }).catch(() => rejectWebSocketUpgrade(socket, 500, 'Upgrade failed'));
447
+ }).catch((error) => { logUpgradeFailure(error); rejectWebSocketUpgrade(socket, 500, 'Upgrade failed'); });
436
448
  return;
437
449
  }
438
450
  try {
@@ -445,8 +457,8 @@ export function createTerminalRuntime({
445
457
  void result.then((sessionToken) => {
446
458
  if (sessionToken) checkOrigin();
447
459
  else rejectWebSocketUpgrade(socket, 401, 'UI authentication required');
448
- }).catch(() => rejectWebSocketUpgrade(socket, 500, 'Upgrade failed'));
449
- } catch { rejectWebSocketUpgrade(socket, 500, 'Upgrade failed'); }
460
+ }).catch((error) => { logUpgradeFailure(error); rejectWebSocketUpgrade(socket, 500, 'Upgrade failed'); });
461
+ } catch (error) { logUpgradeFailure(error); rejectWebSocketUpgrade(socket, 500, 'Upgrade failed'); }
450
462
  };
451
463
  server.on('upgrade', upgradeHandler);
452
464
 
@@ -1,5 +1,7 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import http from 'node:http';
3
+ import net from 'node:net';
4
+ import crypto from 'node:crypto';
3
5
  import fs from 'node:fs';
4
6
  import path from 'node:path';
5
7
  import { describe, expect, it, vi } from 'vitest';
@@ -8,6 +10,24 @@ import { WebSocket } from 'ws';
8
10
  import { createTerminalRuntime } from './runtime.js';
9
11
  import { createTerminalWsControlFrame, readTerminalWsControlFrame } from './terminal-ws-protocol.js';
10
12
 
13
+ // Drives the raw upgrade handler by hand. A `ws` client hides the HTTP response
14
+ // once the 101 lands, and a real client cannot be made to upgrade one socket
15
+ // twice, so the test emits the `upgrade` event directly on a real socket.
16
+ function manualUpgradeRequest(port) {
17
+ const key = crypto.randomBytes(16).toString('base64');
18
+ return {
19
+ url: '/api/terminal/ws',
20
+ method: 'GET',
21
+ headers: {
22
+ host: `127.0.0.1:${port}`,
23
+ upgrade: 'websocket',
24
+ connection: 'Upgrade',
25
+ 'sec-websocket-key': key,
26
+ 'sec-websocket-version': '13',
27
+ },
28
+ };
29
+ }
30
+
11
31
  function createResponse() {
12
32
  return {
13
33
  statusCode: 200,
@@ -764,6 +784,52 @@ describe('terminal runtime', () => {
764
784
  }
765
785
  });
766
786
 
787
+ it('does not write an HTTP rejection onto a socket that already completed its upgrade', async () => {
788
+ const server = http.createServer();
789
+ const refused = [];
790
+ const logged = [];
791
+ const runtime = createRuntime(server, {
792
+ logger: { warn: (...args) => logged.push(args.join(' ')) },
793
+ rejectWebSocketUpgrade(socket, status, reason) {
794
+ refused.push({ status, reason });
795
+ if (!socket || socket.destroyed) return;
796
+ // Mirrors the real helper: it writes an HTTP status line, which is only
797
+ // valid before the handshake switches protocols.
798
+ try { socket.write(`HTTP/1.1 ${status} ${reason}\r\nConnection: close\r\n\r\n`); } catch { /* closed */ }
799
+ try { socket.destroy(); } catch { /* closed */ }
800
+ },
801
+ });
802
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
803
+ const port = server.address().port;
804
+ const socket = net.connect(port, '127.0.0.1');
805
+ await new Promise((resolve, reject) => {
806
+ socket.once('connect', resolve);
807
+ socket.once('error', reject);
808
+ });
809
+ let bytes = '';
810
+ socket.on('data', (chunk) => { bytes += chunk.toString('latin1'); });
811
+ try {
812
+ // First upgrade completes and writes the 101. The second one hits ws's
813
+ // "called more than once" throw, which used to land in the bare catch and
814
+ // append a 500 status line to a connection that is no longer HTTP.
815
+ const req = manualUpgradeRequest(port);
816
+ server.emit('upgrade', req, socket, Buffer.alloc(0));
817
+ server.emit('upgrade', req, socket, Buffer.alloc(0));
818
+ await new Promise((resolve) => setTimeout(resolve, 150));
819
+
820
+ // The socket was destroyed by the failure path, and no HTTP rejection was
821
+ // written for a connection that had already switched protocols.
822
+ expect(refused).toEqual([]);
823
+ expect(bytes).not.toContain('500');
824
+ // The upgrade error was not swallowed: it reached the logger.
825
+ expect(logged.some((line) => /terminal/i.test(line) && /upgrade/i.test(line))).toBe(true);
826
+ } finally {
827
+ socket.destroy();
828
+ await runtime.shutdown();
829
+ await new Promise((resolve) => server.close(resolve));
830
+ }
831
+ });
832
+
767
833
  it('runs snapshot-first attach, scoped I/O, replay, reconnect, and close over a real websocket', async () => {
768
834
  const app = createHttpTestApp();
769
835
  const server = http.createServer(app);
@@ -4,16 +4,19 @@
4
4
  * Public export surface for the Text-to-Speech domain module.
5
5
  */
6
6
 
7
+ /** @public */
7
8
  export {
8
9
  ttsService,
9
10
  TTSService,
10
11
  TTS_VOICES,
11
12
  } from './service.js';
12
13
 
14
+ /** @public */
13
15
  export {
14
16
  summarizeText,
15
17
  sanitizeForTTS,
16
18
  sanitizeForNote,
17
19
  } from '../text/summarization.js';
18
20
 
21
+ /** @public */
19
22
  export { transcribeAudio } from './stt.js';
@@ -10,6 +10,7 @@ import { loadOpenAI } from './openai-sdk.js';
10
10
  import { normalizeCustomOpenAIBaseURL } from './base-url.js';
11
11
 
12
12
  // Voice options from OpenAI
13
+ /** @public */
13
14
  export const TTS_VOICES = [
14
15
  'alloy', 'ash', 'ballad', 'coral', 'echo', 'fable',
15
16
  'nova', 'onyx', 'sage', 'shimmer', 'verse', 'marin', 'cedar'
@@ -47,7 +48,8 @@ async function getOpenAIApiKey() {
47
48
  return null;
48
49
  }
49
50
 
50
- class TTSService {
51
+ /** @public */
52
+ export class TTSService {
51
53
  constructor() {
52
54
  this._client = null;
53
55
  this._lastApiKey = null;
@@ -182,5 +184,5 @@ class TTSService {
182
184
  }
183
185
 
184
186
  // Export singleton instance
187
+ /** @public */
185
188
  export const ttsService = new TTSService();
186
- export { TTSService };
@@ -95,7 +95,9 @@ PR mode uses the same searchable, paginated selector as Changes. Its list loads
95
95
  only while PR mode is visible. Selection ownership and handoff rules are in
96
96
  `packages/ui/src/stores/DOCUMENTATION.md`.
97
97
 
98
- PR sources may include `sourceRepo: { owner, repo }`. It qualifies the cache/job
98
+ PR sources may include `sourceRepo: { owner, repo }`. `owner` is a single
99
+ segment on GitHub and a namespace path on GitLab (`group/subgroup`), matching
100
+ what each provider's project mapper produces. It qualifies the cache/job
99
101
  key as `pr:<owner>/<repo>:<number>`, so equal numbers in different repositories
100
102
  stay apart. Number-only sources keep `pr:<number>` and read the repository behind
101
103
  the bound primary remote. A named repository is honoured only inside the bound
@@ -1,3 +1,4 @@
1
+ import { isString } from '../shared/guards.js';
1
2
  import { getRepositoryRoot } from '../git/service.js';
2
3
  import { describeSmallModel, generateSmallModelText } from '../small-model/index.js';
3
4
  import { buildDigest } from './digest.js';
@@ -101,7 +102,6 @@ const jobs = new Map();
101
102
  const schemaRefusedBy = new Set();
102
103
 
103
104
  const modelKey = (model) => `${model.providerID}/${model.modelID}`;
104
- const isString = (value) => Object.prototype.toString.call(value) === '[object String]';
105
105
  const addReadContext = (result, readContext) => {
106
106
  if (readContext) result.readContext = readContext;
107
107
  return result;
@@ -166,6 +166,7 @@ export function isGenerating(repoRoot, sourceKeyValue, readContext) {
166
166
  * Resolve the pair the job registry is keyed by, for callers that need to look
167
167
  * a job up without doing any diff work.
168
168
  */
169
+ /** @public */
169
170
  export async function getRepositoryRootFor(directory, rawSource, rawReadContext) {
170
171
  const source = parseSource(rawSource);
171
172
  const readContext = readContextForSource(source, rawReadContext);
@@ -642,5 +643,3 @@ async function runGeneration({ directory, source, repoRoot, key, force, explicit
642
643
  ...resolveAgainstCurrent(walkthrough, hunkIndex),
643
644
  }, readContext);
644
645
  }
645
-
646
- export { WalkthroughSourceError };
@@ -25,3 +25,25 @@ describe('repository-qualified PR sources', () => {
25
25
  expect(received).toEqual(['/repo', 42, readContext, { sourceRepo: source.sourceRepo }]);
26
26
  });
27
27
  });
28
+
29
+ describe('GitLab namespace owners', () => {
30
+ it('accepts a subgroup namespace owner so MR walkthrough selection works below the root group', () => {
31
+ const source = parseSource({ kind: 'pr', number: 12, sourceRepo: { owner: 'group/subgroup', repo: 'repo' } });
32
+ expect(source.sourceRepo).toEqual({ owner: 'group/subgroup', repo: 'repo' });
33
+ expect(sourceKey(source)).toBe('pr:group/subgroup/repo:12');
34
+ });
35
+
36
+ it('accepts deeper nesting and dot/underscore segments as GitLab namespaces spell them', () => {
37
+ for (const owner of ['group/sub/sub', 'group.name/sub_group']) {
38
+ const source = parseSource({ kind: 'pr', number: 3, sourceRepo: { owner, repo: 'repo' } });
39
+ expect(source.sourceRepo.owner).toBe(owner);
40
+ }
41
+ });
42
+
43
+ it('keeps rejecting values that are not owner paths', () => {
44
+ for (const owner of ['group/../repo', '/group/repo', 'group/repo/', 'group//repo', '.', 'group/./repo']) {
45
+ expect(() => parseSource({ kind: 'pr', number: 3, sourceRepo: { owner, repo: 'repo' } }))
46
+ .toThrow('pr sources require a valid repository');
47
+ }
48
+ });
49
+ });
@@ -49,7 +49,12 @@ export function parseSource(raw) {
49
49
  if (raw.sourceRepo !== undefined) {
50
50
  const { owner, repo } = raw.sourceRepo ?? {};
51
51
  try {
52
- assert.match(owner, /^[a-zA-Z0-9-]+$/);
52
+ // Owners are a single segment on GitHub and a namespace path on GitLab
53
+ // (subgroups included). Each segment allows the same characters as a
54
+ // repo name with dots kept internal, so "." and ".." can never be a
55
+ // segment. Downstream the owner is only compared against the bound
56
+ // repository's network, never turned into a filesystem path.
57
+ assert.match(owner, /^[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*(?:\/[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*)*$/);
53
58
  assert.match(repo, /^[a-zA-Z0-9_.-]+$/);
54
59
  } catch {
55
60
  throw new WalkthroughSourceError('pr sources require a valid repository');
@@ -1,5 +1,6 @@
1
1
  import { afterEach, describe, expect, it, vi } from 'vitest';
2
2
  import { EventEmitter } from 'node:events';
3
+ import net from 'node:net';
3
4
  import express from 'express';
4
5
  import path from 'path';
5
6
 
@@ -26,6 +27,51 @@ const closeServer = (server) => new Promise((resolve, reject) => {
26
27
  });
27
28
  });
28
29
 
30
+ const sendChunkedPost = (port, route, body, contentType) => new Promise((resolve, reject) => {
31
+ const socket = net.connect(port, '127.0.0.1');
32
+ const chunks = [];
33
+ socket.setTimeout(5000, () => socket.destroy(new Error('Timed out waiting for proxy response')));
34
+ socket.on('connect', () => {
35
+ const payload = Buffer.from(body);
36
+ socket.write([
37
+ `POST ${route} HTTP/1.1`,
38
+ `Host: 127.0.0.1:${port}`,
39
+ 'Connection: close',
40
+ 'Transfer-Encoding: chunked',
41
+ ...(contentType ? [`Content-Type: ${contentType}`] : []),
42
+ '',
43
+ payload.length > 0 ? `${payload.length.toString(16)}\r\n${body}\r\n0\r\n\r\n` : '0\r\n\r\n',
44
+ ].join('\r\n'));
45
+ });
46
+ socket.on('data', (chunk) => chunks.push(chunk));
47
+ socket.on('error', reject);
48
+ socket.on('end', () => {
49
+ const response = Buffer.concat(chunks).toString('utf8');
50
+ const headerEnd = response.indexOf('\r\n\r\n');
51
+ if (headerEnd === -1) {
52
+ reject(new Error('Proxy sent no response headers'));
53
+ return;
54
+ }
55
+ let body = response.slice(headerEnd + 4);
56
+ if (/\r\ntransfer-encoding: chunked\r\n/i.test(response.slice(0, headerEnd) + '\r\n')) {
57
+ const decoded = [];
58
+ let offset = 0;
59
+ while (offset < body.length) {
60
+ const lineEnd = body.indexOf('\r\n', offset);
61
+ const size = Number.parseInt(body.slice(offset, lineEnd), 16);
62
+ if (size === 0) break;
63
+ decoded.push(body.slice(lineEnd + 2, lineEnd + 2 + size));
64
+ offset = lineEnd + 2 + size + 2;
65
+ }
66
+ body = decoded.join('');
67
+ }
68
+ resolve({
69
+ status: Number(response.slice(0, response.indexOf('\r\n')).split(' ')[1]),
70
+ body,
71
+ });
72
+ });
73
+ });
74
+
29
75
  describe('OpenCode proxy SSE forwarding', () => {
30
76
  let upstreamServer;
31
77
  let replacementUpstreamServer;
@@ -509,6 +555,46 @@ describe('OpenCode proxy SSE forwarding', () => {
509
555
  expect(Number(data.contentLength)).toBeGreaterThan(0);
510
556
  });
511
557
 
558
+ it('forwards an unparsed chunked prompt body to OpenCode', async () => {
559
+ const upstream = express();
560
+ upstream.post('/api/session/abc/prompt', express.json(), (req, res) => {
561
+ res.json({
562
+ body: req.body,
563
+ transferEncoding: req.headers['transfer-encoding'] ?? null,
564
+ contentLength: req.headers['content-length'] ?? null,
565
+ });
566
+ });
567
+ upstreamServer = await listen(upstream);
568
+ const upstreamPort = upstreamServer.address().port;
569
+
570
+ const app = express();
571
+ app.use('/api', express.urlencoded({ extended: true }));
572
+ registerOpenCodeProxy(app, {
573
+ fs: {},
574
+ OPEN_CODE_READY_GRACE_MS: 0,
575
+ getRuntime: () => ({
576
+ openCodePort: upstreamPort,
577
+ isOpenCodeReady: true,
578
+ openCodeNotReadySince: 0,
579
+ isRestartingOpenCode: false,
580
+ }),
581
+ getOpenCodeAuthHeaders: () => ({}),
582
+ buildOpenCodeUrl: (requestPath) => `http://127.0.0.1:${upstreamPort}${requestPath}`,
583
+ ensureOpenCodeApiPrefix: () => {},
584
+ });
585
+ proxyServer = await listen(app);
586
+ const proxyPort = proxyServer.address().port;
587
+
588
+ const payload = { parts: [{ type: 'text', text: 'hello' }] };
589
+ const response = await sendChunkedPost(proxyPort, '/api/session/abc/prompt', JSON.stringify(payload), 'application/json');
590
+
591
+ expect(response.status).toBe(200);
592
+ const data = JSON.parse(response.body);
593
+ expect(data.body).toEqual(payload);
594
+ expect(data.transferEncoding).toBe('chunked');
595
+ expect(data.contentLength).toBeNull();
596
+ });
597
+
512
598
  it('replays parsed chunked JSON bodies with a single framing header', async () => {
513
599
  const upstream = express();
514
600
  upstream.post('/api/session/abc/model', express.json(), (req, res) => {
@@ -560,6 +646,47 @@ describe('OpenCode proxy SSE forwarding', () => {
560
646
  expect(Number(data.contentLength)).toBe(JSON.stringify(payload).length);
561
647
  });
562
648
 
649
+ it('forwards an empty chunked request with one framing header', async () => {
650
+ const upstream = express();
651
+ upstream.post('/api/session/abc/interrupt', (req, res) => {
652
+ res.json({
653
+ transferEncoding: req.headers['transfer-encoding'] ?? null,
654
+ contentLength: req.headers['content-length'] ?? null,
655
+ });
656
+ });
657
+ upstreamServer = await listen(upstream);
658
+ const upstreamPort = upstreamServer.address().port;
659
+ const externalBaseUrl = `http://127.0.0.1:${upstreamPort}`;
660
+
661
+ const app = express();
662
+ registerOpenCodeProxy(app, {
663
+ fs: {},
664
+ os: {},
665
+ path,
666
+ OPEN_CODE_READY_GRACE_MS: 0,
667
+ getRuntime: () => ({
668
+ openCodePort: upstreamPort,
669
+ openCodeBaseUrl: externalBaseUrl,
670
+ isOpenCodeReady: true,
671
+ openCodeNotReadySince: 0,
672
+ isRestartingOpenCode: false,
673
+ }),
674
+ getOpenCodeAuthHeaders: () => ({}),
675
+ buildOpenCodeUrl: (requestPath) => `${externalBaseUrl}${requestPath}`,
676
+ ensureOpenCodeApiPrefix: () => {},
677
+ });
678
+ proxyServer = await listen(app);
679
+ const proxyPort = proxyServer.address().port;
680
+
681
+ // A tunnel sends the terminal chunk with no content-length or content-type.
682
+ const response = await sendChunkedPost(proxyPort, '/api/session/abc/interrupt', '');
683
+
684
+ expect(response.status).toBe(200);
685
+ const data = JSON.parse(response.body);
686
+ const framings = [data.transferEncoding !== null, data.contentLength !== null].filter(Boolean);
687
+ expect(framings).toHaveLength(1);
688
+ });
689
+
563
690
  it.each([
564
691
  ['win32', ''],
565
692
  ['win32', '&directory=%2Flink%2Frepo'],
@@ -1,15 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
-
3
- import { startupCommand } from './commands-startup.js';
4
-
5
- const quiet = { quiet: true };
6
-
7
- describe('startup command service controls', () => {
8
- for (const action of ['start', 'stop', 'restart']) {
9
- it(`${action}s the installed startup service without changing installation`, async () => {
10
- const controlStartupService = vi.fn(() => ({ supported: true, enabled: true, active: action !== 'stop', platform: 'linux' }));
11
- await startupCommand(quiet, action, { controlStartupService });
12
- expect(controlStartupService).toHaveBeenCalledWith(action);
13
- });
14
- }
15
- });
@@ -1 +0,0 @@
1
- import{j as a}from"./vendor-react-BUCujlKS.js";import{I as l,a as I}from"./useInlineCommentController-CrjSGCpi.js";function E(g){const{drafts:x,editingDraftId:d,commentText:o,onTextChange:r,selection:n,isDragging:u,fileLabel:s,newWidgetId:f,mapDraftToRange:p,onSave:m,onCancel:c,onEdit:C,onDelete:h}=g,t=[];for(const e of x){const i=p(e);if(e.id===d){t.push({afterLine:i.end,id:`edit-${e.id}`,content:a.jsx(l,{initialText:o,onTextChange:r,fileLabel:s,lineRange:i,isEditing:!0,onSave:m,onCancel:c},`edit-${e.id}`)});continue}t.push({afterLine:i.end,id:`card-${e.id}`,content:a.jsx(I,{draft:e,onEdit:()=>C(e),onDelete:()=>h(e)},`card-${e.id}`)})}if(n&&!d&&!u){const e={...n,start:Math.min(n.start,n.end),end:Math.max(n.start,n.end)};t.push({afterLine:e.end,id:f,content:a.jsx(l,{initialText:o,onTextChange:r,fileLabel:s,lineRange:e,isEditing:!1,onSave:m,onCancel:c},f)})}return t}export{E as b};