@vantran-se/openchamber-web 1.23.0 → 1.24.3

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 (362) hide show
  1. package/bin/cli.test.js +29 -6
  2. package/bin/lib/cli-http.js +5 -1
  3. package/bin/lib/commands-status.js +5 -0
  4. package/dist/assets/{CodeMirrorCommentWidgets-DFg2wnpo.js → CodeMirrorCommentWidgets-BjJHrzmU.js} +1 -1
  5. package/dist/assets/DiffView-COVQhWGd.js +4 -0
  6. package/dist/assets/FilesView-B1V5JxFX.js +13 -0
  7. package/dist/assets/GitView-CDOJy5Ay.js +6 -0
  8. package/dist/assets/GuestAttachDialog-D3S0Rjic.js +1 -0
  9. package/dist/assets/{JsonTreeView-BKdsjg5B.js → JsonTreeView-D0C9nU7K.js} +1 -1
  10. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  11. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  12. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  13. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  14. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  15. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  16. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  17. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  18. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  19. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  20. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  21. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  22. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  23. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  24. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  25. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  26. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  27. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  28. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  29. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  30. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  31. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  32. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  33. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  34. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  35. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  36. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  37. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  38. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  39. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  40. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  41. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  42. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  43. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  44. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  45. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  46. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  47. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  48. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  49. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  50. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  51. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  52. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  53. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  54. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  55. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  56. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  57. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  58. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  59. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  60. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  61. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  62. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  63. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  64. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  65. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  66. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  67. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  68. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  69. package/dist/assets/LinearIssuesView-D7FKg1XJ.js +1 -0
  70. package/dist/assets/{MarkdownImageGallery-_5gEVr0X.js → MarkdownImageGallery-BrFjAVEf.js} +1 -1
  71. package/dist/assets/MarkdownRendererImpl-DWepTafh.js +12 -0
  72. package/dist/assets/{OnboardingScreen-DdP4_nuc.js → OnboardingScreen-Dyxxiqdt.js} +1 -1
  73. package/dist/assets/{PatchDiff-am3SAss2.js → PatchDiff-DnEhjXcC.js} +1 -1
  74. package/dist/assets/PierreDiffViewer-DyxZVVVR.js +210 -0
  75. package/dist/assets/{PlanView-BoJxdRZe.js → PlanView-LW2z_EAP.js} +4 -4
  76. package/dist/assets/PluginPane-B4tKno2h.js +3 -0
  77. package/dist/assets/SettingsView-DEaMulN9.js +29 -0
  78. package/dist/assets/{SettingsWindow-DVTdDSol.js → SettingsWindow-FLDJXOgQ.js} +1 -1
  79. package/dist/assets/SubmoduleDiffSummary-CvVwD-Vi.js +1 -0
  80. package/dist/assets/ToolOutputDialog-BiP0x2Rw.js +15 -0
  81. package/dist/assets/{ToolPartDiffPreview-C7WLk5i9.js → ToolPartDiffPreview-CkR4msKP.js} +2 -2
  82. package/dist/assets/{VirtualizedFileDiff-DcdgkH-x.js → VirtualizedFileDiff-0pyr_mHY.js} +4 -4
  83. package/dist/assets/WalkthroughView-yNj4nOP5.js +4 -0
  84. package/dist/assets/appThemeRegistry-BuUKhbYQ.js +1 -0
  85. package/dist/assets/commitAndPush-DvJKa3Ks.js +1 -0
  86. package/dist/assets/{computeVirtualFileMetrics-C8byMnRh.js → computeVirtualFileMetrics-DxTBMuu-.js} +2 -2
  87. package/dist/assets/de-DgxgFyGG.js +10 -0
  88. package/dist/assets/es-BFsZj5F_.js +16 -0
  89. package/dist/assets/fr-jdZ5KKQi.js +14 -0
  90. package/dist/assets/{index-BB7ryz8y.js → index-CKHjPKbq.js} +1 -1
  91. package/dist/assets/index-Cbci4i1W.js +40 -0
  92. package/dist/assets/index-DqaMWNrC.css +1 -0
  93. package/dist/assets/{isDiffPlainText-45ujMPGb.js → isDiffPlainText-D7FkBMP6.js} +1 -1
  94. package/dist/assets/{isFilePlainText-Cc4KFSYi.js → isFilePlainText-CL2vsrIZ.js} +1 -1
  95. package/dist/assets/ja-C5kKrhO4.js +16 -0
  96. package/dist/assets/ko-DXDTT_0O.js +16 -0
  97. package/dist/assets/main-CZALtVek.js +896 -0
  98. package/dist/assets/main-CgXMBNvG.js +2 -0
  99. package/dist/assets/{markdown-shiki.worker-D5fBU-lw.js → markdown-shiki.worker-CahLvHIq.js} +5 -5
  100. package/dist/assets/markdownCore-Dh7WEsdM.js +5 -0
  101. package/dist/assets/miniChat-CwTmDixi.js +2 -0
  102. package/dist/assets/mobile-CLf2vCtO.js +2 -0
  103. package/dist/assets/patchFileDiff-Bsba8KFp.js +13 -0
  104. package/dist/assets/pl-D-7x4Y8Q.js +16 -0
  105. package/dist/assets/pt-BR-CSBZrHa8.js +16 -0
  106. package/dist/assets/renderElectronMiniChatApp-C5gAFBk0.js +2 -0
  107. package/dist/assets/renderMobileApp-V4hYd32H.js +25 -0
  108. package/dist/assets/runtimeEndpointReset-DhlHCn-2.js +25 -0
  109. package/dist/assets/scheduledTasksApi-DzjbLd8P.js +1 -0
  110. package/dist/assets/shikiHighlight-8_3csmr6.js +1 -0
  111. package/dist/assets/surface-B6n7wJtt.js +1 -0
  112. package/dist/assets/textMateThemeFromAppTheme-CHo2fCgW.js +1 -0
  113. package/dist/assets/tr-Dr-oJ22L.js +7 -0
  114. package/dist/assets/uk-9Nx83UcI.js +16 -0
  115. package/dist/assets/useAppFontEffects-khZXpFL_.js +7333 -0
  116. package/dist/assets/useInlineCommentController-DkQ_gqpn.js +2 -0
  117. package/dist/assets/useRootScrollLock-DdvHf9Pt.js +2 -0
  118. package/dist/assets/{vendor-base-ui-DdV8vhHn.js → vendor-base-ui-CsXYnR5c.js} +1 -1
  119. package/dist/assets/{vendor-codemirror-language-data-DzlSIu_W.js → vendor-codemirror-language-data-CdzUmUyZ.js} +2 -2
  120. package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +1 -0
  121. package/dist/assets/vendor-katex-Cp579UoJ.css +1 -0
  122. package/dist/assets/{vendor-motion-dom-D_o3BTI3.js → vendor-motion-dom-DcCgeG69.js} +1 -1
  123. package/dist/assets/{vendor-remixicon-react-Dil0zKKT.js → vendor-remixicon-react-BDmGmFCA.js} +1 -1
  124. package/dist/assets/vendor-strip-json-comments-5dJlOkP1.js +4 -0
  125. package/dist/assets/{vendor-tanstack-react-virtual-g05Cx3z9.js → vendor-tanstack-react-virtual-ClYN8CzB.js} +1 -1
  126. package/dist/assets/{vendor-tanstack-virtual-core-Dubg10g5.js → vendor-tanstack-virtual-core-BewVQryB.js} +1 -1
  127. package/dist/assets/vendor-zod-CgAdXWZO.js +39 -0
  128. package/dist/assets/{web-components-CdGfR2XJ.js → web-components-B8IcVQAA.js} +2 -2
  129. package/dist/assets/zh-CN-Bxy_Hj3r.js +16 -0
  130. package/dist/assets/zh-TW-0BB4gpHM.js +16 -0
  131. package/dist/index.html +6 -4
  132. package/dist/mini-chat.html +6 -4
  133. package/dist/mobile.html +6 -4
  134. package/package.json +12 -9
  135. package/server/built-in-extensions/registry.json +4 -0
  136. package/server/index.d.ts +2 -0
  137. package/server/index.js +47 -23
  138. package/server/lib/agent-memory/runtime.js +7 -2
  139. package/server/lib/agent-memory/runtime.test.js +22 -0
  140. package/server/lib/agent-tool/DOCUMENTATION.md +17 -4
  141. package/server/lib/agent-tool/runtime.js +62 -15
  142. package/server/lib/agent-tool/runtime.test.js +88 -2
  143. package/server/lib/changelog/update-notes.js +2 -1
  144. package/server/lib/changelog/update-notes.test.js +19 -0
  145. package/server/lib/event-stream/DOCUMENTATION.md +26 -0
  146. package/server/lib/event-stream/delta-coalescer.js +0 -0
  147. package/server/lib/event-stream/delta-coalescer.test.js +292 -0
  148. package/server/lib/event-stream/global-hub.js +68 -13
  149. package/server/lib/event-stream/global-hub.test.js +182 -0
  150. package/server/lib/event-stream/global-ws-bridge.js +16 -18
  151. package/server/lib/event-stream/index.js +4 -0
  152. package/server/lib/event-stream/protocol.js +21 -7
  153. package/server/lib/event-stream/runtime.js +9 -6
  154. package/server/lib/event-stream/runtime.test.js +60 -0
  155. package/server/lib/fs/DOCUMENTATION.md +10 -1
  156. package/server/lib/fs/git-process.test.js +77 -0
  157. package/server/lib/fs/routes.js +58 -5
  158. package/server/lib/fs/routes.test.js +116 -0
  159. package/server/lib/fs/search.js +2 -1
  160. package/server/lib/git/DOCUMENTATION.md +30 -6
  161. package/server/lib/git/routes.js +37 -11
  162. package/server/lib/git/routes.test.js +78 -0
  163. package/server/lib/git/serial-refresh.js +93 -0
  164. package/server/lib/git/serial-refresh.test.js +112 -0
  165. package/server/lib/git/service.js +588 -74
  166. package/server/lib/git/service.test.js +492 -0
  167. package/server/lib/github/DOCUMENTATION.md +5 -0
  168. package/server/lib/github/pr-status.js +5 -5
  169. package/server/lib/github/pr-status.test.js +1 -1
  170. package/server/lib/github/routes.js +6 -6
  171. package/server/lib/guests/DOCUMENTATION.md +68 -0
  172. package/server/lib/guests/auth-store.js +184 -0
  173. package/server/lib/guests/auth-store.test.js +112 -0
  174. package/server/lib/guests/background.test.js +83 -0
  175. package/server/lib/guests/builtins.js +34 -0
  176. package/server/lib/guests/builtins.test.js +174 -0
  177. package/server/lib/guests/catalog.js +477 -0
  178. package/server/lib/guests/catalog.test.js +390 -0
  179. package/server/lib/guests/clone.js +297 -0
  180. package/server/lib/guests/extract-zip.js +136 -0
  181. package/server/lib/guests/extract-zip.test.js +109 -0
  182. package/server/lib/guests/files.js +308 -0
  183. package/server/lib/guests/files.test.js +262 -0
  184. package/server/lib/guests/grant-scope.js +81 -0
  185. package/server/lib/guests/grant-scope.test.js +75 -0
  186. package/server/lib/guests/host-session.js +73 -0
  187. package/server/lib/guests/host-session.test.js +30 -0
  188. package/server/lib/guests/html-styles.js +6 -0
  189. package/server/lib/guests/html-styles.test.js +45 -0
  190. package/server/lib/guests/html-tokens.js +73 -0
  191. package/server/lib/guests/html-tokens.test.js +53 -0
  192. package/server/lib/guests/install.js +406 -0
  193. package/server/lib/guests/install.test.js +338 -0
  194. package/server/lib/guests/oauth.js +501 -0
  195. package/server/lib/guests/oauth.test.js +481 -0
  196. package/server/lib/guests/persist.js +336 -0
  197. package/server/lib/guests/persist.test.js +288 -0
  198. package/server/lib/guests/request.js +112 -0
  199. package/server/lib/guests/request.test.js +206 -0
  200. package/server/lib/guests/routes.js +838 -0
  201. package/server/lib/guests/service.js +538 -0
  202. package/server/lib/guests/service.test.js +277 -0
  203. package/server/lib/guests/sockets.js +129 -0
  204. package/server/lib/guests/sockets.test.js +110 -0
  205. package/server/lib/guests/ssh-install.test.js +188 -0
  206. package/server/lib/guests/storage.js +68 -0
  207. package/server/lib/guests/storage.test.js +59 -0
  208. package/server/lib/guests/updates.js +294 -0
  209. package/server/lib/guests/updates.test.js +334 -0
  210. package/server/lib/guests/upload.js +50 -0
  211. package/server/lib/guests/upload.test.js +131 -0
  212. package/server/lib/linear/DOCUMENTATION.md +1 -1
  213. package/server/lib/message-queue/DOCUMENTATION.md +17 -1
  214. package/server/lib/message-queue/runtime.js +34 -1
  215. package/server/lib/message-queue/runtime.test.js +83 -6
  216. package/server/lib/notifications/DOCUMENTATION.md +1 -1
  217. package/server/lib/notifications/emitter-runtime.js +4 -3
  218. package/server/lib/notifications/routes.js +5 -0
  219. package/server/lib/openchamber-control/DOCUMENTATION.md +6 -0
  220. package/server/lib/openchamber-sessions/routes.js +62 -29
  221. package/server/lib/openchamber-sessions/routes.test.js +96 -0
  222. package/server/lib/opencode/DOCUMENTATION.md +73 -5
  223. package/server/lib/opencode/bootstrap-runtime.js +1 -0
  224. package/server/lib/opencode/core-routes.js +39 -1
  225. package/server/lib/opencode/env-runtime.js +5 -1
  226. package/server/lib/opencode/env-runtime.test.js +72 -0
  227. package/server/lib/opencode/feature-routes-runtime.js +30 -1
  228. package/server/lib/opencode/lifecycle-process.test.js +84 -0
  229. package/server/lib/opencode/lifecycle.js +58 -70
  230. package/server/lib/opencode/lifecycle.test.js +29 -0
  231. package/server/lib/opencode/managed-plugin-config.js +1 -1
  232. package/server/lib/opencode/openchamber-routes.js +72 -14
  233. package/server/lib/opencode/openchamber-routes.test.js +71 -3
  234. package/server/lib/opencode/proxy.js +33 -0
  235. package/server/lib/opencode/session-runtime.js +68 -0
  236. package/server/lib/opencode/session-runtime.test.js +43 -0
  237. package/server/lib/opencode/settings-helpers.js +11 -0
  238. package/server/lib/opencode/settings-helpers.test.js +24 -2
  239. package/server/lib/opencode/settings-normalization-runtime.js +2 -0
  240. package/server/lib/opencode/settings-normalization-runtime.test.js +13 -0
  241. package/server/lib/opencode/settings-registry.json +18 -0
  242. package/server/lib/opencode/settings-runtime.js +36 -9
  243. package/server/lib/opencode/settings-runtime.test.js +158 -2
  244. package/server/lib/opencode/shutdown-runtime.js +3 -0
  245. package/server/lib/opencode/shutdown-runtime.test.js +24 -0
  246. package/server/lib/opencode/theme-archive.js +53 -0
  247. package/server/lib/opencode/theme-catalog.js +171 -0
  248. package/server/lib/opencode/theme-catalog.test.js +125 -0
  249. package/server/lib/opencode/theme-import.test.js +144 -0
  250. package/server/lib/opencode/theme-runtime.js +100 -28
  251. package/server/lib/opencode/theme-runtime.test.js +15 -0
  252. package/server/lib/permission-auto-accept/DOCUMENTATION.md +8 -0
  253. package/server/lib/permission-auto-accept/runtime.js +13 -0
  254. package/server/lib/permission-auto-accept/runtime.test.js +37 -1
  255. package/server/lib/project-context/DOCUMENTATION.md +13 -3
  256. package/server/lib/project-context/runtime.js +8 -2
  257. package/server/lib/project-context/runtime.test.js +54 -0
  258. package/server/lib/projects/DOCUMENTATION.md +2 -1
  259. package/server/lib/projects/project-config.js +40 -11
  260. package/server/lib/projects/project-config.test.js +97 -0
  261. package/server/lib/projects/project-id.js +26 -0
  262. package/server/lib/quota/DOCUMENTATION.md +0 -1
  263. package/server/lib/quota/providers/index.js +0 -7
  264. package/server/lib/realtime-proxy-terminal.test.js +150 -0
  265. package/server/lib/realtime-proxy.js +2 -0
  266. package/server/lib/relay/DOCUMENTATION.md +20 -0
  267. package/server/lib/relay/host-client.js +7 -0
  268. package/server/lib/relay/host-client.test.js +14 -0
  269. package/server/lib/routing/DOCUMENTATION.md +83 -0
  270. package/server/lib/routing/defaults.js +98 -0
  271. package/server/lib/routing/feature-flag.js +11 -0
  272. package/server/lib/routing/history.js +34 -0
  273. package/server/lib/routing/jev.js +98 -0
  274. package/server/lib/routing/routes.http.test.js +100 -0
  275. package/server/lib/routing/routes.js +81 -0
  276. package/server/lib/routing/runtime.js +217 -0
  277. package/server/lib/routing/runtime.test.js +188 -0
  278. package/server/lib/routing/store.js +249 -0
  279. package/server/lib/routing/store.test.js +96 -0
  280. package/server/lib/scheduled-tasks/DOCUMENTATION.md +9 -1
  281. package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +28 -0
  282. package/server/lib/scheduled-tasks/runtime.js +14 -4
  283. package/server/lib/scheduled-tasks/runtime.test.js +67 -0
  284. package/server/lib/security/request-security.js +9 -5
  285. package/server/lib/security/request-security.test.js +37 -0
  286. package/server/lib/session-assist/DOCUMENTATION.md +131 -66
  287. package/server/lib/session-assist/context.js +127 -0
  288. package/server/lib/session-assist/context.test.js +132 -0
  289. package/server/lib/session-assist/prompt.js +54 -0
  290. package/server/lib/session-assist/runtime.js +111 -257
  291. package/server/lib/session-assist/runtime.test.js +203 -0
  292. package/server/lib/small-model/DOCUMENTATION.md +14 -2
  293. package/server/lib/small-model/call.js +37 -6
  294. package/server/lib/small-model/call.test.js +182 -0
  295. package/server/lib/terminal/DOCUMENTATION.md +23 -2
  296. package/server/lib/terminal/runtime.js +27 -5
  297. package/server/lib/terminal/runtime.test.js +67 -0
  298. package/server/lib/terminal/shutdown.js +106 -0
  299. package/server/lib/terminal/shutdown.test.js +84 -0
  300. package/server/lib/tts/service.test.js +78 -0
  301. package/server/lib/ui-auth/DOCUMENTATION.md +8 -0
  302. package/server/lib/ui-auth/session-cookie.js +51 -0
  303. package/server/lib/ui-auth/session-cookie.test.js +46 -0
  304. package/server/lib/ui-auth/ui-auth.js +56 -17
  305. package/server/lib/ui-auth/ui-auth.test.js +96 -0
  306. package/server/lib/walkthrough/DOCUMENTATION.md +15 -5
  307. package/server/lib/walkthrough/pr-source.test.js +21 -0
  308. package/server/lib/walkthrough/pull-request.js +7 -4
  309. package/server/lib/walkthrough/pull-request.test.js +16 -0
  310. package/server/lib/walkthrough/routes.js +19 -0
  311. package/server/lib/walkthrough/routes.test.js +26 -0
  312. package/server/lib/walkthrough/sources.js +13 -2
  313. package/server/opencode-worktree-readiness.test.js +130 -0
  314. package/dist/assets/DiffView-3Dn4QHbu.js +0 -4
  315. package/dist/assets/DirtyBranchSwitchDialog-CwOerqVi.js +0 -1
  316. package/dist/assets/FilesView-BdTOpew9.js +0 -17
  317. package/dist/assets/GitView-Dqf9vHJh.js +0 -6
  318. package/dist/assets/LinearIssuesView-CmbeL7I6.js +0 -1
  319. package/dist/assets/MarkdownRendererImpl-wVZlx4dd.js +0 -12
  320. package/dist/assets/PierreDiffViewer-DCsydlKs.js +0 -161
  321. package/dist/assets/SettingsView-CcM8pF5O.js +0 -28
  322. package/dist/assets/ToolOutputDialog-BgfTbDZ4.js +0 -15
  323. package/dist/assets/WalkthroughView-BEVUDxuw.js +0 -4
  324. package/dist/assets/appThemeRegistry-BK0dHKa6.js +0 -1
  325. package/dist/assets/de-C8jV7J9y.js +0 -10
  326. package/dist/assets/es-BI6J1M5R.js +0 -16
  327. package/dist/assets/fr-D6oDFt7E.js +0 -14
  328. package/dist/assets/index-BqhiCmYS.js +0 -39
  329. package/dist/assets/index-FqSOBz0J.css +0 -1
  330. package/dist/assets/ja-C_4i5EQu.js +0 -16
  331. package/dist/assets/ko-BFbOTpbq.js +0 -16
  332. package/dist/assets/main-C9ETLKFc.js +0 -2
  333. package/dist/assets/main-CJmb2dF8.js +0 -930
  334. package/dist/assets/markdownCore-DVuZpnli.js +0 -5
  335. package/dist/assets/miniChat-B4Ytoq2n.js +0 -2
  336. package/dist/assets/mobile-BUT6-lo1.js +0 -2
  337. package/dist/assets/patchFileDiff-8j1apICU.js +0 -13
  338. package/dist/assets/pl-COFvT56l.js +0 -16
  339. package/dist/assets/pt-BR-DtVOO2Lj.js +0 -16
  340. package/dist/assets/registerCustomTheme-BBXUCRnG.js +0 -1
  341. package/dist/assets/renderElectronMiniChatApp-B6vUG-vF.js +0 -2
  342. package/dist/assets/renderMobileApp-BNgHraq6.js +0 -25
  343. package/dist/assets/runtimeEndpointReset-B8LYpWvr.js +0 -17
  344. package/dist/assets/scheduledTasksApi-GC4epIYA.js +0 -1
  345. package/dist/assets/shikiHighlight-BJISeZsQ.js +0 -1
  346. package/dist/assets/surface-BvRzS58U.js +0 -1
  347. package/dist/assets/tr-B8Q0Go2l.js +0 -7
  348. package/dist/assets/uk-Dk4a-Fn-.js +0 -16
  349. package/dist/assets/useAppFontEffects-DpFNefUA.js +0 -7284
  350. package/dist/assets/useGitComparison-Dfxaqd0s.js +0 -1
  351. package/dist/assets/useInlineCommentController-CscsE4t-.js +0 -2
  352. package/dist/assets/useRootScrollLock-BaCvuRlT.js +0 -1
  353. package/dist/assets/vendor-framer-motion-livSKyyV.js +0 -1
  354. package/dist/assets/vendor-zod-BPAANqvH.js +0 -39
  355. package/dist/assets/zh-CN-B_BXxG1Q.js +0 -16
  356. package/dist/assets/zh-TW-DLrl1SPZ.js +0 -16
  357. package/server/lib/mcp-reconnect/DOCUMENTATION.md +0 -55
  358. package/server/lib/mcp-reconnect/runtime.js +0 -122
  359. package/server/lib/mcp-reconnect/runtime.test.js +0 -140
  360. package/server/lib/quota/providers/crof.js +0 -96
  361. package/server/lib/quota/providers/crof.test.js +0 -98
  362. /package/dist/assets/{vendor-katex-k3Ppq1Ha.js → vendor-katex-CHD9BdDO.js} +0 -0
@@ -2,6 +2,28 @@ import { describe, expect, it, vi } from 'vitest';
2
2
 
3
3
  import { createGlobalMessageStreamHub } from './global-hub.js';
4
4
 
5
+ it('bounds a contiguous replay suffix by UTF-8 bytes and event count', async () => {
6
+ const blocks = Array.from({ length: 8 }, (_, i) => `id: e${i}\ndata: ${JSON.stringify({ type: 'message', properties: { text: '界'.repeat(40) } })}\n\n`);
7
+ const received = [];
8
+ const hub = createGlobalMessageStreamHub({
9
+ buildOpenCodeUrl: path => `http://127.0.0.1:4096${path}`,
10
+ getOpenCodeAuthHeaders: () => ({}), replayLimit: 3, replayByteLimit: 550,
11
+ upstreamReconnectDelayMs: 60_000,
12
+ fetchImpl: async () => createSseResponse({ blocks }),
13
+ });
14
+ hub.subscribeEvent(event => received.push(event.eventId));
15
+ try {
16
+ hub.start();
17
+ await waitForAssertion(() => expect(received).toHaveLength(8));
18
+ expect(hub.replayAfter('e0')).toBeNull();
19
+ expect(hub.replayAfter('e5')).toBeNull();
20
+ const tail = hub.replayAfter('e6');
21
+ expect(tail.map(entry => entry.eventId)).toEqual(['e7']);
22
+ expect(Buffer.byteLength(tail[0].serializedFrame) * 2).toBeLessThanOrEqual(550);
23
+ expect(Buffer.byteLength(tail[0].serializedFrame) * 3).toBeGreaterThan(550);
24
+ } finally { hub.stop(); }
25
+ });
26
+
5
27
  function createSseResponse({ blocks = [] } = {}) {
6
28
  const encoder = new TextEncoder();
7
29
  let index = 0;
@@ -40,6 +62,166 @@ async function waitForAssertion(assertion) {
40
62
  throw lastError;
41
63
  }
42
64
 
65
+ const deltaBlock = (id, text, partID = 'prt_a') => `id: ${id}\ndata: ${JSON.stringify({
66
+ id, type: 'message.part.delta',
67
+ properties: { sessionID: 'ses_1', messageID: 'msg_1', partID, field: 'text', delta: text },
68
+ })}\n\n`;
69
+
70
+ const createDeltaHub = ({ blocks, deltaCoalesceWindowMs }) => createGlobalMessageStreamHub({
71
+ buildOpenCodeUrl: (pathname) => `http://127.0.0.1:4096${pathname}`,
72
+ getOpenCodeAuthHeaders: () => ({}),
73
+ upstreamReconnectDelayMs: 60_000,
74
+ deltaCoalesceWindowMs,
75
+ fetchImpl: async () => createSseResponse({ blocks }),
76
+ });
77
+
78
+ // What a browser holds after applying frames in order: text per part, and the
79
+ // text each part had when a snapshot barrier passed.
80
+ const applyFrames = (frames) => {
81
+ const text = {};
82
+ const barriers = [];
83
+ for (const frame of frames) {
84
+ const payload = frame.payload;
85
+ if (payload.type === 'message.part.delta') {
86
+ text[payload.properties.partID] = (text[payload.properties.partID] ?? '') + payload.properties.delta;
87
+ } else {
88
+ barriers.push({ id: payload.id, seen: { ...text } });
89
+ }
90
+ }
91
+ return { text, barriers };
92
+ };
93
+
94
+ describe('delta coalescing in the global hub', () => {
95
+ it('delivers the same text in far fewer frames', async () => {
96
+ const words = Array.from({ length: 120 }, (_, index) => `w${index} `);
97
+ const hub = createDeltaHub({ blocks: words.map((word, index) => deltaBlock(`e${String(index).padStart(4, '0')}`, word)) });
98
+ const received = [];
99
+ hub.subscribeEvent((event) => received.push(event));
100
+ try {
101
+ hub.start();
102
+ await waitForAssertion(() => expect(applyFrames(received).text.prt_a).toBe(words.join('')));
103
+ expect(received.length).toBeLessThanOrEqual(3);
104
+ expect(received.at(-1).eventId).toBe('e0119');
105
+ } finally { hub.stop(); }
106
+ });
107
+
108
+ it('resumes from any cursor without losing or repeating text', async () => {
109
+ const blocks = [];
110
+ let id = 0;
111
+ const nextId = () => `e${String(id++).padStart(4, '0')}`;
112
+ for (let index = 0; index < 40; index += 1) blocks.push(deltaBlock(nextId(), `a${index}.`, index % 3 === 0 ? 'prt_b' : 'prt_a'));
113
+ const snapshotId = nextId();
114
+ blocks.push(`id: ${snapshotId}\ndata: ${JSON.stringify({ id: snapshotId, type: 'message.part.updated', properties: { part: { id: 'prt_a', messageID: 'msg_1' } } })}\n\n`);
115
+ for (let index = 0; index < 40; index += 1) blocks.push(deltaBlock(nextId(), `b${index}.`));
116
+
117
+ const hub = createDeltaHub({ blocks });
118
+ const received = [];
119
+ hub.subscribeEvent((event) => received.push(event));
120
+ try {
121
+ hub.start();
122
+ const expectedA = [...Array(40).keys()].filter((index) => index % 3 !== 0).map((index) => `a${index}.`).join('')
123
+ + [...Array(40).keys()].map((index) => `b${index}.`).join('');
124
+ await waitForAssertion(() => expect(applyFrames(received).text.prt_a).toBe(expectedA));
125
+ expect(received.length).toBeLessThan(blocks.length / 4);
126
+
127
+ const complete = applyFrames(received);
128
+ // The snapshot barrier saw exactly the text that arrived before it.
129
+ expect(complete.barriers).toHaveLength(1);
130
+ expect(complete.barriers[0].seen.prt_a).toBe([...Array(40).keys()].filter((index) => index % 3 !== 0).map((index) => `a${index}.`).join(''));
131
+
132
+ // A socket that drops after any frame reconnects with that frame's id.
133
+ for (let cut = 0; cut < received.length; cut += 1) {
134
+ const tail = hub.replayAfter(received[cut].eventId);
135
+ expect(tail).not.toBeNull();
136
+ const replayed = tail.map((entry) => JSON.parse(entry.serializedFrame));
137
+ expect(applyFrames([...received.slice(0, cut + 1), ...replayed])).toEqual(complete);
138
+ }
139
+ } finally { hub.stop(); }
140
+ });
141
+
142
+ // OpenCode 1.18 sends no SSE ids at all. Before the hub numbered such events
143
+ // itself the replay buffer stayed empty and every reconnect lost its gap.
144
+ it('numbers id-less upstream events so a reconnect resumes from any cursor', async () => {
145
+ const idless = (type, properties) => `data: ${JSON.stringify({ type, properties })}\n\n`;
146
+ const blocks = [];
147
+ for (let index = 0; index < 30; index += 1) {
148
+ blocks.push(idless('message.part.delta', { sessionID: 'ses_1', messageID: 'msg_1', partID: 'prt_a', field: 'text', delta: `a${index}.` }));
149
+ }
150
+ blocks.push(idless('message.part.updated', { part: { id: 'prt_a', messageID: 'msg_1' } }));
151
+ for (let index = 0; index < 30; index += 1) {
152
+ blocks.push(idless('message.part.delta', { sessionID: 'ses_1', messageID: 'msg_1', partID: 'prt_a', field: 'text', delta: `b${index}.` }));
153
+ }
154
+
155
+ const hub = createDeltaHub({ blocks });
156
+ const received = [];
157
+ hub.subscribeEvent((event) => received.push(event));
158
+ try {
159
+ hub.start();
160
+ const expected = [...Array(30).keys()].map((index) => `a${index}.`).join('') + [...Array(30).keys()].map((index) => `b${index}.`).join('');
161
+ await waitForAssertion(() => expect(applyFrames(received).text.prt_a).toBe(expected));
162
+
163
+ const ids = received.map((event) => event.eventId);
164
+ expect(ids.every((eventId) => typeof eventId === 'string' && eventId.length > 0)).toBe(true);
165
+ expect(new Set(ids).size).toBe(ids.length);
166
+ expect(ids).toEqual([...ids].sort());
167
+
168
+ const complete = applyFrames(received);
169
+ for (let cut = 0; cut < received.length; cut += 1) {
170
+ const tail = hub.replayAfter(received[cut].eventId);
171
+ expect(tail).not.toBeNull();
172
+ const replayed = tail.map((entry) => JSON.parse(entry.serializedFrame));
173
+ expect(replayed.every((frame) => typeof frame.eventId === 'string')).toBe(true);
174
+ expect(applyFrames([...received.slice(0, cut + 1), ...replayed])).toEqual(complete);
175
+ }
176
+
177
+ // A cursor minted by another server process must miss, never match by
178
+ // sequence number: the bridge then reports replayReset and the client
179
+ // repairs from HTTP instead of trusting a wrong tail.
180
+ const foreign = ids[0].replace(/^oc-[^-]+-/, 'oc-00000000-');
181
+ expect(foreign).not.toBe(ids[0]);
182
+ expect(hub.replayAfter(foreign)).toBeNull();
183
+ } finally { hub.stop(); }
184
+ });
185
+
186
+ it('keeps pending text for replay when the hub stops', async () => {
187
+ const hub = createDeltaHub({
188
+ deltaCoalesceWindowMs: 60_000,
189
+ blocks: ['one ', 'two ', 'three'].map((text, index) => deltaBlock(`e${index}`, text)),
190
+ });
191
+ const received = [];
192
+ hub.subscribeEvent((event) => received.push(event));
193
+ hub.start();
194
+ // The window is a minute, so only the leading delta has been delivered.
195
+ await waitForAssertion(() => expect(received).toHaveLength(1));
196
+ await new Promise((resolve) => setTimeout(resolve, 20));
197
+
198
+ hub.stop();
199
+
200
+ const tail = hub.replayAfter('e0').map((entry) => JSON.parse(entry.serializedFrame));
201
+ expect(applyFrames(tail).text.prt_a).toBe('two three');
202
+ expect(tail.at(-1).eventId).toBe('e2');
203
+ });
204
+
205
+ it('commits pending text on demand, so a client readied later starts after it', async () => {
206
+ const hub = createDeltaHub({
207
+ deltaCoalesceWindowMs: 60_000,
208
+ blocks: ['one ', 'two ', 'three'].map((text, index) => deltaBlock(`e${index}`, text)),
209
+ });
210
+ const received = [];
211
+ hub.subscribeEvent((event) => received.push(event));
212
+ try {
213
+ hub.start();
214
+ await waitForAssertion(() => expect(received).toHaveLength(1));
215
+ await new Promise((resolve) => setTimeout(resolve, 20));
216
+
217
+ hub.flushPending();
218
+
219
+ expect(applyFrames(received).text.prt_a).toBe('one two three');
220
+ expect(hub.replayAfter('e2')).toEqual([]);
221
+ } finally { hub.stop(); }
222
+ });
223
+ });
224
+
43
225
  describe('createGlobalMessageStreamHub', () => {
44
226
  it('continues fanout when an event subscriber throws', async () => {
45
227
  const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
@@ -1,4 +1,4 @@
1
- import { sendMessageStreamWsEvent, sendMessageStreamWsFrame } from './protocol.js';
1
+ import { sendMessageStreamWsEvent, sendMessageStreamWsFrame, sendSerializedMessageStreamWsFrame, serializeMessageStreamWsEvent } from './protocol.js';
2
2
 
3
3
  function shouldTriggerUpstreamHealthCheck(upstream) {
4
4
  if (!upstream) {
@@ -31,12 +31,9 @@ export function createGlobalMessageStreamWsBridge({
31
31
  wsClients.delete(socket);
32
32
  };
33
33
 
34
- const replayEvents = (socket, requestedLastEventId) => {
35
- for (const entry of globalHub.replayAfter(requestedLastEventId)) {
36
- const sent = sendMessageStreamWsEvent(socket, entry.payload, {
37
- directory: entry.directory,
38
- eventId: entry.eventId,
39
- });
34
+ const replayEvents = (socket, entries) => {
35
+ for (const entry of entries) {
36
+ const sent = sendSerializedMessageStreamWsFrame(socket, entry.serializedFrame);
40
37
  if (!sent) {
41
38
  removeClient(socket);
42
39
  return;
@@ -49,10 +46,11 @@ export function createGlobalMessageStreamWsBridge({
49
46
  return;
50
47
  }
51
48
 
52
- const sent = sendMessageStreamWsFrame(socket, {
53
- type: 'ready',
54
- scope: 'global',
55
- });
49
+ globalHub.flushPending();
50
+ const replay = globalHub.replayAfter(requestedLastEventId);
51
+ const ready = { type: 'ready', scope: 'global' };
52
+ if (replay === null) ready.replayReset = true;
53
+ const sent = sendMessageStreamWsFrame(socket, ready);
56
54
  if (!sent) {
57
55
  removeClient(socket);
58
56
  return;
@@ -60,7 +58,7 @@ export function createGlobalMessageStreamWsBridge({
60
58
 
61
59
  readyClients.add(socket);
62
60
  wsClients.add(socket);
63
- replayEvents(socket, requestedLastEventId);
61
+ if (replay !== null) replayEvents(socket, replay);
64
62
  };
65
63
 
66
64
  const stopHubIfUnused = () => {
@@ -88,26 +86,26 @@ export function createGlobalMessageStreamWsBridge({
88
86
  }
89
87
  };
90
88
 
91
- const unsubscribeEvent = globalHub.subscribeEvent(({ payload, directory, eventId }) => {
89
+ const unsubscribeEvent = globalHub.subscribeEvent((event) => {
90
+ const { payload } = event;
92
91
  for (const socket of Array.from(clients)) {
93
92
  if (!readyClients.has(socket)) {
94
93
  continue;
95
94
  }
96
- const sent = sendMessageStreamWsEvent(socket, payload, {
97
- directory,
98
- eventId,
99
- });
95
+ const sent = sendSerializedMessageStreamWsFrame(socket, event.serialize());
100
96
  if (!sent) {
101
97
  removeClient(socket);
102
98
  }
103
99
  }
104
100
 
105
101
  processForwardedEventPayload(payload, (syntheticPayload) => {
102
+ if (readyClients.size === 0) return;
103
+ const serializedFrame = serializeMessageStreamWsEvent(syntheticPayload, { directory: 'global' });
106
104
  for (const socket of Array.from(clients)) {
107
105
  if (!readyClients.has(socket)) {
108
106
  continue;
109
107
  }
110
- const sent = sendMessageStreamWsEvent(socket, syntheticPayload, { directory: 'global' });
108
+ const sent = sendSerializedMessageStreamWsFrame(socket, serializedFrame);
111
109
  if (!sent) {
112
110
  removeClient(socket);
113
111
  }
@@ -7,6 +7,10 @@ export {
7
7
  createGlobalMessageStreamHub,
8
8
  } from './global-hub.js';
9
9
 
10
+ export {
11
+ resolveDeltaCoalesceWindowMs,
12
+ } from './delta-coalescer.js';
13
+
10
14
  export {
11
15
  DEFAULT_UPSTREAM_STALL_TIMEOUT_MS,
12
16
  UPSTREAM_STALL_TIMEOUT_CONCURRENT_MS,
@@ -71,6 +71,14 @@ export function parseSseEventEnvelope(block) {
71
71
  }
72
72
 
73
73
  export function sendMessageStreamWsFrame(socket, payload) {
74
+ try {
75
+ return sendSerializedMessageStreamWsFrame(socket, JSON.stringify(payload));
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+
81
+ export function sendSerializedMessageStreamWsFrame(socket, serializedFrame) {
74
82
  if (!socket || socket.readyState !== 1) {
75
83
  return false;
76
84
  }
@@ -86,7 +94,7 @@ export function sendMessageStreamWsFrame(socket, payload) {
86
94
  }
87
95
 
88
96
  try {
89
- socket.send(JSON.stringify(payload));
97
+ socket.send(serializedFrame);
90
98
  const bufferedAfter = typeof socket.bufferedAmount === 'number' ? socket.bufferedAmount : 0;
91
99
  if (bufferedAfter > MESSAGE_STREAM_WS_MAX_BUFFERED_BYTES) {
92
100
  try {
@@ -124,10 +132,16 @@ export function sendMessageStreamWsFrame(socket, payload) {
124
132
  }
125
133
 
126
134
  export function sendMessageStreamWsEvent(socket, payload, options = {}) {
127
- return sendMessageStreamWsFrame(socket, {
128
- type: 'event',
129
- payload,
130
- ...(typeof options.eventId === 'string' && options.eventId.length > 0 ? { eventId: options.eventId } : {}),
131
- ...(typeof options.directory === 'string' && options.directory.length > 0 ? { directory: options.directory } : {}),
132
- });
135
+ try {
136
+ return sendSerializedMessageStreamWsFrame(socket, serializeMessageStreamWsEvent(payload, options));
137
+ } catch {
138
+ return false;
139
+ }
140
+ }
141
+
142
+ export function serializeMessageStreamWsEvent(payload, options = {}) {
143
+ const frame = { type: 'event', payload };
144
+ if (typeof options.eventId === 'string' && options.eventId.length > 0) frame.eventId = options.eventId;
145
+ if (typeof options.directory === 'string' && options.directory.length > 0) frame.directory = options.directory;
146
+ return JSON.stringify(frame);
133
147
  }
@@ -5,7 +5,8 @@ import {
5
5
  MESSAGE_STREAM_DIRECTORY_WS_PATH,
6
6
  MESSAGE_STREAM_GLOBAL_WS_PATH,
7
7
  MESSAGE_STREAM_WS_HEARTBEAT_INTERVAL_MS,
8
- sendMessageStreamWsEvent,
8
+ serializeMessageStreamWsEvent,
9
+ sendSerializedMessageStreamWsFrame,
9
10
  } from './protocol.js';
10
11
  import { createGlobalMessageStreamHub } from './global-hub.js';
11
12
  import { createGlobalMessageStreamWsBridge } from './global-ws-bridge.js';
@@ -28,20 +29,22 @@ export function createGlobalUiEventBroadcaster({
28
29
  }
29
30
 
30
31
  if (hasSseClients) {
32
+ const serializedPayload = JSON.stringify(payload);
31
33
  for (const res of sseClients) {
32
34
  try {
33
- writeSseEvent(res, payload);
35
+ writeSseEvent(res, payload, serializedPayload);
34
36
  } catch {
35
37
  }
36
38
  }
37
39
  }
38
40
 
39
41
  if (hasWsClients) {
42
+ const serializedFrame = serializeMessageStreamWsEvent(payload, {
43
+ directory: typeof options.directory === 'string' && options.directory.length > 0 ? options.directory : 'global',
44
+ eventId: typeof options.eventId === 'string' && options.eventId.length > 0 ? options.eventId : undefined,
45
+ });
40
46
  for (const socket of Array.from(wsClients)) {
41
- const sent = sendMessageStreamWsEvent(socket, payload, {
42
- directory: typeof options.directory === 'string' && options.directory.length > 0 ? options.directory : 'global',
43
- eventId: typeof options.eventId === 'string' && options.eventId.length > 0 ? options.eventId : undefined,
44
- });
47
+ const sent = sendSerializedMessageStreamWsFrame(socket, serializedFrame);
45
48
  if (!sent) {
46
49
  wsClients.delete(socket);
47
50
  }
@@ -2,6 +2,66 @@ import { EventEmitter } from 'node:events';
2
2
  import { describe, expect, it } from 'vitest';
3
3
 
4
4
  import { createGlobalUiEventBroadcaster, createMessageStreamWsRuntime } from './runtime.js';
5
+ import { createGlobalMessageStreamHub } from './global-hub.js';
6
+ import { createNotificationEmitterRuntime } from '../notifications/emitter-runtime.js';
7
+
8
+ it('serializes a UI broadcast once per wire format and isolates failed clients', () => {
9
+ let serializations = 0;
10
+ const payload = { toJSON() { serializations++; return { type: 'openchamber:test', properties: { value: 'same' } }; } };
11
+ const writes = [];
12
+ const sseClients = new Set(Array.from({ length: 4 }, () => ({ write: text => writes.push(text) })));
13
+ const sockets = Array.from({ length: 4 }, () => ({ readyState: 1, bufferedAmount: 0, send: text => writes.push(text) }));
14
+ const failed = { readyState: 1, bufferedAmount: 0, send() { throw new Error('closed'); } };
15
+ const wsClients = new Set([...sockets, failed]);
16
+ const { writeSseEvent } = createNotificationEmitterRuntime({});
17
+ const broadcast = createGlobalUiEventBroadcaster({ sseClients, wsClients, writeSseEvent });
18
+ broadcast(payload);
19
+ expect(serializations).toBe(2);
20
+ expect(writes).toHaveLength(8);
21
+ expect(wsClients.has(failed)).toBe(false);
22
+ expect(wsClients.size).toBe(4);
23
+ expect(writes[0]).toBe(writes[3]);
24
+ expect(writes[4]).toBe(writes[7]);
25
+ sseClients.clear(); wsClients.clear();
26
+ broadcast(payload);
27
+ expect(serializations).toBe(2);
28
+ });
29
+
30
+ it('delivers oversized events live, but reports a replay gap instead of replaying across it', async () => {
31
+ const hub = createGlobalMessageStreamHub({
32
+ buildOpenCodeUrl: path => `http://127.0.0.1:4096${path}`,
33
+ getOpenCodeAuthHeaders: () => ({}),
34
+ replayByteLimit: 512,
35
+ fetchImpl: async (_url, options) => createSseResponse({
36
+ signal: options.signal, holdOpen: true,
37
+ blocks: [
38
+ 'id: first\ndata: {"type":"first","properties":{}}\n\n',
39
+ `id: large\ndata: ${JSON.stringify({ type: 'large', properties: { text: '界'.repeat(1024) } })}\n\n`,
40
+ 'id: last\ndata: {"type":"last","properties":{}}\n\n',
41
+ ],
42
+ }),
43
+ });
44
+ const runtime = createMessageStreamWsRuntime({
45
+ server: new EventEmitter(), globalEventHub: hub, wsClients: new Set(),
46
+ uiAuthController: null, isRequestOriginAllowed: async () => true,
47
+ rejectWebSocketUpgrade() {}, buildOpenCodeUrl: path => `http://127.0.0.1:4096${path}`,
48
+ getOpenCodeAuthHeaders: () => ({}), processForwardedEventPayload() {},
49
+ });
50
+ const first = new FakeSocket();
51
+ const reconnect = new FakeSocket();
52
+ try {
53
+ runtime.wsServer.emit('connection', first, { url: '/api/global/event/ws' });
54
+ await expect.poll(() => first.sent.filter(frame => frame.type === 'event').length).toBe(3);
55
+ expect(first.sent.find(frame => frame.eventId === 'large').payload.properties.text).toHaveLength(1024);
56
+ expect(hub.replayAfter('first')).toBeNull();
57
+ expect(hub.replayAfter('last')).toEqual([]);
58
+ runtime.wsServer.emit('connection', reconnect, { url: '/api/global/event/ws?lastEventId=first' });
59
+ await expect.poll(() => reconnect.sent.length).toBeGreaterThan(0);
60
+ expect(reconnect.sent).toEqual([{ type: 'ready', scope: 'global', replayReset: true }]);
61
+ } finally {
62
+ first.close(); reconnect.close(); hub.stop(); await runtime.close();
63
+ }
64
+ });
5
65
 
6
66
  class FakeSocket extends EventEmitter {
7
67
  constructor() {
@@ -36,6 +36,14 @@ Own filesystem API behavior for the web server runtime, including workspace-boun
36
36
  - Returns `{ searchFilesystemFiles(rootPath, options) }`.
37
37
  - Supports fuzzy matching, hidden-file handling, and optional `git check-ignore` filtering.
38
38
 
39
+ Both search and directory listing discard `git check-ignore` stderr at spawn.
40
+ They consume stdout for ignore matches. Never create an unread stderr pipe:
41
+ Git diagnostics can fill it and block the child indefinitely, so repeated
42
+ searches accumulate live Git processes. `git-process.test.js` exercises both
43
+ paths with real OS pipes, twelve concurrent checks, and 2 MiB of diagnostics
44
+ per child. Successful completion must preserve ignore filtering and reap all
45
+ twelve children.
46
+
39
47
  ## Composition contract with `index.js`
40
48
  - `index.js` provides composition-time dependencies only (platform primitives + callbacks such as `resolveProjectDirectory`, `normalizeDirectoryPath`, and `buildAugmentedPath`).
41
49
  - `index.js` no longer owns FS route handlers or FS exec job state.
@@ -43,7 +51,8 @@ Own filesystem API behavior for the web server runtime, including workspace-boun
43
51
  ## Notes for contributors
44
52
  - Keep filesystem policy (workspace root checks, error mapping, exec timeout behavior) inside this module, not in the composition root.
45
53
  - Workspace checks accept, besides the active workspace and its worktrees, the **managed roots**: the OpenChamber config root and the managed chats root (`managedChatsRoot` dependency; `OPENCHAMBER_CHATS_DIR` upstream, default `<config root>/chats`). Chat worktrees may legitimately live outside every project workspace.
46
- - `GET /api/fs/home` answers `{ home, chatsRoot }`. `chatsRoot` is the server-resolved managed chats root; clients must use it instead of joining `home` + the well-known segment (a relocated root does not contain that segment).
54
+ - `GET /api/fs/home` preserves `{ home, chatsRoot }` and adds `canonicalChatsRoot` and `canonicalLegacyChatsRoot`, resolved by the server's filesystem. If a root does not exist yet, it resolves the nearest existing ancestor and appends the missing segments, without creating directories. Errors resolving the configured root fail the request. A failed legacy lookup warns and omits only that optional alias, retaining exact legacy matching without blocking an accessible relocated root. Clients use confirmed aliases for exact membership while keeping the original roots as folder/scope identities. `chatsRoot` is the server-resolved managed chats root; clients must use it instead of joining `home` + the well-known segment (a relocated root does not contain that segment).
55
+ - Workspace authorization accepts both the configured managed paths and their filesystem-confirmed canonical roots. Canonical lookup runs only when lexical workspace/managed-root checks fail. One failed managed-root lookup cannot reject an independently authorized root or worktree. Path comparisons remain case-sensitive on POSIX: distinct Linux directories named `chats` and `Chats` never become aliases through string folding. Shared UI keeps exact root membership for classification and directory cleanup.
47
56
  - Filesystem `EPERM`/`EACCES` failures use the stable `reason: "os-permission"` response marker. Policy denials such as workspace-boundary or missing-grant failures must not use that marker because a native folder picker cannot remediate them.
48
57
  - `GET /api/fs/directory-stat?path=...` uses one `stat` without listing contents or resolving project topology. It follows the same authenticated directory-discovery path policy as `/api/fs/list`, including targets outside the active workspace. A directory returns `{ isDirectory: true }`; `ENOENT` returns `not-found`, and a file or `ENOTDIR` returns `not-directory`. Permission and other failures remain distinct from a missing path. VS Code explicitly returns 501, so the shared client treats its probe as unknown.
49
58
  - Read-only routes authorize the requested path against the workspace before resolving symlinks. A symlink reached through the workspace may therefore target a file outside it, while a directly requested outside path still requires an exact-path grant. Write routes keep canonical-target boundary checks.
@@ -0,0 +1,77 @@
1
+ import { spawn } from 'node:child_process';
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { describe, expect, it } from 'vitest';
7
+ import { createFsSearchRuntime } from './search.js';
8
+ import { registerFsRoutes } from './routes.js';
9
+
10
+ // Use real OS pipes. A mocked data event cannot reproduce a full stderr pipe.
11
+ const noisyGit = `
12
+ const fs = require('node:fs');
13
+ const chunk = Buffer.alloc(65536, 'x');
14
+ for (let i = 0; i < 32; i++) fs.writeSync(2, chunk);
15
+ fs.writeSync(1, 'ignored.txt\\n');
16
+ `;
17
+
18
+ describe('filesystem Git process ownership', () => {
19
+ for (const operation of ['search', 'list']) {
20
+ it(`${operation} completes repeated checks with more diagnostics than an OS pipe can hold`, async () => {
21
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'oc-git-pipes-'));
22
+ const children = [];
23
+ const exits = [];
24
+ const launch = (binary, args, options) => {
25
+ expect(binary).toBe('git');
26
+ expect(args).toContain('check-ignore');
27
+ expect(args).toContain('ignored.txt');
28
+ const child = spawn(process.execPath, ['-e', noisyGit], options);
29
+ children.push(child);
30
+ exits.push(new Promise((resolve) => child.once('close', (code) => resolve(code))));
31
+ return child;
32
+ };
33
+ let deadline;
34
+ try {
35
+ await fs.writeFile(path.join(root, 'ignored.txt'), 'ignored');
36
+ await fs.writeFile(path.join(root, 'visible.txt'), 'visible');
37
+ const dependencies = { fsPromises: fs, path, spawn: launch, resolveGitBinaryForSpawn: () => 'git' };
38
+ const search = createFsSearchRuntime(dependencies);
39
+ const routes = new Map();
40
+ registerFsRoutes({
41
+ get(route, handler) { routes.set(route, handler); }, post() {}, put() {}, delete() {},
42
+ }, {
43
+ ...dependencies, os, crypto, normalizeDirectoryPath: (value) => value,
44
+ resolveProjectDirectory: async () => ({ directory: root }),
45
+ buildAugmentedPath: () => process.env.PATH,
46
+ openchamberUserConfigRoot: root,
47
+ gitCheckIgnoreTimeoutMs: 10_000,
48
+ });
49
+ const work = Array.from({ length: 12 }, async () => {
50
+ if (operation === 'search') {
51
+ const files = await search.searchFilesystemFiles(root, { query: '', limit: 10, respectGitignore: true });
52
+ expect(files.map((file) => file.name)).toEqual(['visible.txt']);
53
+ } else {
54
+ const response = { status() { return this; }, json(value) { this.body = value; } };
55
+ await routes.get('/api/fs/list')({ query: { path: root, respectGitignore: 'true' } }, response);
56
+ expect(response.body.entries.map((entry) => entry.name)).toEqual(['visible.txt']);
57
+ }
58
+ });
59
+ await Promise.race([
60
+ Promise.all(work),
61
+ new Promise((_, reject) => { deadline = setTimeout(() => reject(new Error('Git blocked on unread diagnostics')), 5000); }),
62
+ ]);
63
+ expect(children).toHaveLength(12);
64
+ expect(await Promise.all(exits)).toEqual(Array(12).fill(0));
65
+ for (const child of children) expect(child.exitCode).toBe(0);
66
+ } finally {
67
+ clearTimeout(deadline);
68
+ for (const child of children) {
69
+ child.stderr?.resume();
70
+ if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL');
71
+ }
72
+ await Promise.all(exits);
73
+ await fs.rm(root, { recursive: true, force: true });
74
+ }
75
+ }, 15_000);
76
+ }
77
+ });