@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
@@ -8,6 +8,7 @@ import { loadSourceSections, parseSource, sourceKey } from '../walkthrough/sourc
8
8
  import { registerGitRoutes } from './routes.js';
9
9
 
10
10
  import {
11
+ unsupportedRepositoryRootReason,
11
12
  checkoutBranch,
12
13
  checkoutCommit,
13
14
  cherryPick,
@@ -21,8 +22,10 @@ import {
21
22
  getCommitFiles,
22
23
  getLog,
23
24
  getStatus,
25
+ getTrackingBranch,
24
26
  getWorktrees,
25
27
  isGitRepository,
28
+ observeWorktreeTopology,
26
29
  populateWorktreeWithLockRecovery,
27
30
  removeWorktree,
28
31
  resolvePrimaryWorktreeRoot,
@@ -32,14 +35,18 @@ import {
32
35
  revertCommit,
33
36
  setLocalIdentity,
34
37
  stageFiles,
38
+ subscribeWorktreeTopologyChanges,
35
39
  unstageFiles,
36
40
  applyHunk,
37
41
  getDiff,
42
+ getPathDiff,
43
+ revertFile,
38
44
  getUntrackedDiffs,
39
45
  getFileDiff,
40
46
  validateWorktreeCreate,
41
47
  parseBranchCreationSource,
42
48
  getRangeFiles,
49
+ push,
43
50
  } from './service.js';
44
51
 
45
52
  // ---------------------------------------------------------------------------
@@ -125,6 +132,23 @@ async function createTempRepo() {
125
132
  // resolveBaseRefForLog
126
133
  // ---------------------------------------------------------------------------
127
134
 
135
+ describe('unsupportedRepositoryRootReason', () => {
136
+ it('rejects a repository rooted at a filesystem root or the home directory', () => {
137
+ const home = path.join(os.tmpdir(), 'unsupported-root-home');
138
+ expect(unsupportedRepositoryRootReason('/', home)).toBe('filesystem-root');
139
+ expect(unsupportedRepositoryRootReason(path.parse(process.cwd()).root, home)).toBe('filesystem-root');
140
+ expect(unsupportedRepositoryRootReason(home, home)).toBe('home');
141
+ expect(unsupportedRepositoryRootReason(`${home}${path.sep}`, home)).toBe('home');
142
+ });
143
+
144
+ it('accepts an ordinary project root, including one directly under home', () => {
145
+ const home = path.join(os.tmpdir(), 'unsupported-root-home');
146
+ expect(unsupportedRepositoryRootReason(path.join(home, 'project'), home)).toBeNull();
147
+ expect(unsupportedRepositoryRootReason(path.join(os.tmpdir(), 'repo'), home)).toBeNull();
148
+ expect(unsupportedRepositoryRootReason('', home)).toBeNull();
149
+ });
150
+ });
151
+
128
152
  describe('resolveBaseRefForLog', () => {
129
153
  it('returns the local ref unchanged when it exists, even if origin also exists', async () => {
130
154
  const checkRef = async (ref) => ref === 'main' || ref === 'refs/remotes/origin/main';
@@ -505,6 +529,131 @@ describe('symlink diffs', () => {
505
529
  });
506
530
  });
507
531
 
532
+ // ---------------------------------------------------------------------------
533
+ // Status paths that are not plain files (#3586)
534
+ // ---------------------------------------------------------------------------
535
+
536
+ describe.runIf(canRunGit())('diffs for status paths that are not plain files', () => {
537
+ const callDiffRoute = async (endpoint, query) => {
538
+ const routes = new Map();
539
+ registerGitRoutes({ get: (url, handler) => routes.set(url, handler), post() {}, put() {}, delete() {} });
540
+ let status = 200;
541
+ let body;
542
+ await routes.get(`/api/git/${endpoint}`)({ query }, {
543
+ status(value) { status = value; return this; },
544
+ json(value) { body = value; },
545
+ });
546
+ return { status, body };
547
+ };
548
+
549
+ const createRepositoryWithSubmodule = () => {
550
+ const { repository } = createRepositoryWithRemote();
551
+ const library = createTempDir();
552
+ runGit(library, ['init', '-b', 'main']);
553
+ runGit(library, ['config', 'user.email', 'test@example.com']);
554
+ runGit(library, ['config', 'user.name', 'Test']);
555
+ fs.writeFileSync(path.join(library, 'lib.txt'), 'lib\n');
556
+ runGit(library, ['add', '.']);
557
+ runGit(library, ['commit', '-m', 'lib']);
558
+ runGit(repository, ['-c', 'protocol.file.allow=always', 'submodule', 'add', library, 'sub']);
559
+ runGit(repository, ['commit', '-m', 'add submodule']);
560
+ return { repository, recorded: runGit(repository, ['rev-parse', 'HEAD:sub']).trim() };
561
+ };
562
+
563
+ it('answers 404 with a code when a listed file is gone before its diff is requested', async () => {
564
+ const { repository } = createRepositoryWithRemote();
565
+ for (const endpoint of ['diff', 'file-diff']) {
566
+ const { status, body } = await callDiffRoute(endpoint, { directory: repository, path: 'removed.txt' });
567
+ expect(status).toBe(404);
568
+ expect(body).toEqual({ code: 'path_not_found', error: 'Path not found in working tree, index, or HEAD: removed.txt' });
569
+ }
570
+ });
571
+
572
+ it('answers 422 for a nested repository that status lists as a directory', async () => {
573
+ const { repository } = createRepositoryWithRemote();
574
+ const nested = path.join(repository, 'nested');
575
+ fs.mkdirSync(nested);
576
+ runGit(nested, ['init', '-b', 'main']);
577
+ fs.writeFileSync(path.join(nested, 'inner.txt'), 'inner\n');
578
+ expect((await getStatus(repository)).files).toContainEqual(expect.objectContaining({ path: 'nested/' }));
579
+
580
+ for (const endpoint of ['diff', 'file-diff']) {
581
+ const { status, body } = await callDiffRoute(endpoint, { directory: repository, path: 'nested/' });
582
+ expect(status).toBe(422);
583
+ expect(body.code).toBe('nested_repository');
584
+ }
585
+ await expect(revertFile(repository, 'nested/')).rejects.toMatchObject({ code: 'nested_repository' });
586
+ expect(fs.existsSync(path.join(nested, 'inner.txt'))).toBe(true);
587
+ });
588
+
589
+ it('describes a submodule whose checked-out commit moved', async () => {
590
+ const { repository, recorded } = createRepositoryWithSubmodule();
591
+ const submodulePath = path.join(repository, 'sub');
592
+ runGit(submodulePath, ['config', 'user.email', 'test@example.com']);
593
+ runGit(submodulePath, ['config', 'user.name', 'Test']);
594
+ runGit(submodulePath, ['commit', '--allow-empty', '-m', 'moved']);
595
+ const moved = runGit(submodulePath, ['rev-parse', 'HEAD']).trim();
596
+ const submodule = { headCommit: recorded, indexCommit: recorded, worktreeCommit: moved, hasTrackedChanges: false, hasUntrackedFiles: false, hasConflict: false };
597
+
598
+ const patch = await callDiffRoute('diff', { directory: repository, path: 'sub' });
599
+ expect(patch.status).toBe(200);
600
+ expect(patch.body.diff).toContain(`+Subproject commit ${moved}`);
601
+ expect(patch.body.submodule).toEqual(submodule);
602
+
603
+ const split = await callDiffRoute('file-diff', { directory: repository, path: 'sub' });
604
+ expect(split.body).toEqual({
605
+ original: `Subproject commit ${recorded}\n`,
606
+ modified: `Subproject commit ${moved}\n`,
607
+ path: 'sub',
608
+ isBinary: false,
609
+ submodule,
610
+ });
611
+ });
612
+
613
+ it('reports a submodule merge conflict instead of an unchanged commit', async () => {
614
+ const { repository } = createRepositoryWithRemote();
615
+ const library = createTempDir();
616
+ runGit(library, ['init', '-b', 'main']);
617
+ runGit(library, ['config', 'user.email', 'test@example.com']);
618
+ runGit(library, ['config', 'user.name', 'Test']);
619
+ runGit(library, ['commit', '--allow-empty', '-m', 'base']);
620
+ runGit(library, ['checkout', '-b', 'left']);
621
+ runGit(library, ['commit', '--allow-empty', '-m', 'left']);
622
+ runGit(library, ['checkout', '-b', 'right', 'main']);
623
+ runGit(library, ['commit', '--allow-empty', '-m', 'right']);
624
+ runGit(library, ['checkout', 'main']);
625
+ runGit(repository, ['-c', 'protocol.file.allow=always', 'submodule', 'add', library, 'sub']);
626
+ runGit(repository, ['commit', '-m', 'add submodule']);
627
+ const submodulePath = path.join(repository, 'sub');
628
+ for (const [branch, commit] of [['other', 'right'], ['next', 'left']]) {
629
+ if (branch === 'other') runGit(repository, ['checkout', '-b', 'other']);
630
+ else runGit(repository, ['checkout', 'next']);
631
+ runGit(submodulePath, ['checkout', commit]);
632
+ runGit(repository, ['add', 'sub']);
633
+ runGit(repository, ['commit', '-m', `move to ${commit}`]);
634
+ }
635
+ expect(() => runGit(repository, ['merge', 'other'])).toThrow();
636
+
637
+ const { submodule } = await getPathDiff(repository, { path: 'sub' });
638
+ expect(submodule).toMatchObject({
639
+ headCommit: runGit(repository, ['rev-parse', 'HEAD:sub']).trim(),
640
+ indexCommit: null,
641
+ hasConflict: true,
642
+ });
643
+ });
644
+
645
+ it('reports untracked files inside a submodule even though its patch is empty', async () => {
646
+ const { repository, recorded } = createRepositoryWithSubmodule();
647
+ fs.writeFileSync(path.join(repository, 'sub', 'scratch.txt'), 'scratch\n');
648
+
649
+ const result = await getPathDiff(repository, { path: 'sub' });
650
+ expect(result).toEqual({
651
+ diff: '',
652
+ submodule: { headCommit: recorded, indexCommit: recorded, worktreeCommit: recorded, hasTrackedChanges: false, hasUntrackedFiles: true, hasConflict: false },
653
+ });
654
+ });
655
+ });
656
+
508
657
  // ---------------------------------------------------------------------------
509
658
  // getStatus
510
659
  // ---------------------------------------------------------------------------
@@ -601,6 +750,142 @@ describe('getStatus', () => {
601
750
  process.chdir(previousCwd);
602
751
  }
603
752
  });
753
+
754
+ it('scopes diff stats by staged and working instead of combining a partially staged file', async () => {
755
+ if (!canRunGit()) return;
756
+
757
+ const repo = createTempDir();
758
+ runGit(repo, ['init', '-b', 'main']);
759
+ runGit(repo, ['config', 'user.email', 'test@example.com']);
760
+ runGit(repo, ['config', 'user.name', 'Test User']);
761
+ const file = 'test.txt';
762
+ const filePath = path.join(repo, file);
763
+ fs.writeFileSync(filePath, 'one\ntwo\nthree\n');
764
+ runGit(repo, ['add', file]);
765
+ runGit(repo, ['commit', '-m', 'initial']);
766
+
767
+ // Stage one new line, then keep editing without staging another.
768
+ fs.writeFileSync(filePath, 'one\ntwo\nthree\nstaged\n');
769
+ runGit(repo, ['add', file]);
770
+ fs.writeFileSync(filePath, 'one\ntwo\nthree\nstaged\nworking\n');
771
+
772
+ const status = await getStatus(repo);
773
+
774
+ expect(status.diffStats.staged[file]).toEqual({ insertions: 1, deletions: 0 });
775
+ expect(status.diffStats.working[file]).toEqual({ insertions: 1, deletions: 0 });
776
+ });
777
+
778
+ it('scopes untracked files to working stats and staged additions to staged stats', async () => {
779
+ if (!canRunGit()) return;
780
+
781
+ const repo = createTempDir();
782
+ runGit(repo, ['init', '-b', 'main']);
783
+ runGit(repo, ['config', 'user.email', 'test@example.com']);
784
+ runGit(repo, ['config', 'user.name', 'Test User']);
785
+ fs.writeFileSync(path.join(repo, 'tracked.txt'), 'tracked\n');
786
+ runGit(repo, ['add', 'tracked.txt']);
787
+ runGit(repo, ['commit', '-m', 'initial']);
788
+
789
+ fs.writeFileSync(path.join(repo, 'untracked.txt'), 'a\nb\n');
790
+ fs.writeFileSync(path.join(repo, 'staged.txt'), 'c\nd\ne\n');
791
+ runGit(repo, ['add', 'staged.txt']);
792
+
793
+ const status = await getStatus(repo);
794
+
795
+ expect(status.diffStats.working['untracked.txt']).toEqual({ insertions: 2, deletions: 0 });
796
+ expect(status.diffStats.staged['staged.txt']).toEqual({ insertions: 3, deletions: 0 });
797
+ expect(status.diffStats.working['staged.txt']).toBeUndefined();
798
+ });
799
+ });
800
+
801
+ describe('push', () => {
802
+ it('publishes the current branch with an upstream and leaves other local branches alone', async () => {
803
+ if (!canRunGit()) return;
804
+
805
+ const remote = createTempDir();
806
+ const repo = createTempDir();
807
+ runGit(remote, ['init', '--bare']);
808
+ runGit(repo, ['init', '-b', 'main']);
809
+ runGit(repo, ['config', 'user.email', 'test@example.com']);
810
+ runGit(repo, ['config', 'user.name', 'Test User']);
811
+ fs.writeFileSync(path.join(repo, 'README.md'), '# Test\n');
812
+ runGit(repo, ['add', 'README.md']);
813
+ runGit(repo, ['commit', '-m', 'Initial commit']);
814
+ runGit(repo, ['remote', 'add', 'fork', remote]);
815
+ runGit(repo, ['branch', 'unrelated']);
816
+ runGit(repo, ['checkout', '-b', 'feature']);
817
+ fs.writeFileSync(path.join(repo, 'feature.txt'), 'published\n');
818
+ runGit(repo, ['add', 'feature.txt']);
819
+ runGit(repo, ['commit', '-m', 'Add feature']);
820
+
821
+ const published = await push(repo, { remote: 'fork' });
822
+ expect(published.pushed).toEqual([{ local: 'refs/heads/feature', remote: 'fork' }]);
823
+
824
+ expect(runGit(repo, ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']).trim()).toBe('fork/feature');
825
+ expect(runGit(remote, ['rev-parse', 'refs/heads/feature']).trim()).toBe(runGit(repo, ['rev-parse', 'HEAD']).trim());
826
+ expect(() => runGit(remote, ['rev-parse', 'refs/heads/unrelated'])).toThrow();
827
+ expect(() => runGit(remote, ['rev-parse', 'refs/heads/main'])).toThrow();
828
+
829
+ expect((await push(repo)).pushed).toEqual([]);
830
+ fs.appendFileSync(path.join(repo, 'feature.txt'), 'next commit\n');
831
+ runGit(repo, ['commit', '-am', 'Update feature']);
832
+ expect((await push(repo)).pushed).toEqual([{ local: 'refs/heads/feature', remote: 'fork' }]);
833
+ expect(runGit(remote, ['rev-parse', 'feature']).trim()).toBe(runGit(repo, ['rev-parse', 'HEAD']).trim());
834
+
835
+ runGit(repo, ['reset', '--hard', 'HEAD~1']);
836
+ await expect(push(repo)).rejects.toThrow();
837
+ expect((await push(repo, { options: ['--force-with-lease'] })).pushed)
838
+ .toEqual([{ local: 'refs/heads/feature', remote: 'fork' }]);
839
+ expect(runGit(remote, ['rev-parse', 'feature']).trim()).toBe(runGit(repo, ['rev-parse', 'HEAD']).trim());
840
+ });
841
+
842
+ it.each(['remote.pushDefault', 'branch.next.pushRemote'])('preserves the %s destination independently of the fetch remote', async (key) => {
843
+ const { repository, remote: upstream } = createRepositoryWithRemote({ remoteName: 'upstream', defaultBranch: 'next' });
844
+ const fork = createTempDir();
845
+ runGit(fork, ['init', '--bare']);
846
+ runGit(repository, ['remote', 'add', 'fork', fork]);
847
+ runGit(repository, ['branch', '--set-upstream-to=upstream/next']);
848
+ if (key === 'branch.next.pushRemote') runGit(repository, ['config', 'remote.pushDefault', 'upstream']);
849
+ runGit(repository, ['config', key, 'fork']);
850
+ const upstreamHead = runGit(upstream, ['rev-parse', 'next']).trim();
851
+ fs.appendFileSync(path.join(repository, 'README.md'), 'fork change\n');
852
+ runGit(repository, ['commit', '-am', 'Change for fork']);
853
+
854
+ expect((await push(repository)).pushed).toEqual([{ local: 'refs/heads/next', remote: 'fork' }]);
855
+ expect(runGit(fork, ['rev-parse', 'next']).trim()).toBe(runGit(repository, ['rev-parse', 'HEAD']).trim());
856
+ expect(runGit(upstream, ['rev-parse', 'next']).trim()).toBe(upstreamHead);
857
+ expect(readBranchConfig(repository, 'next', 'remote')).toBe('upstream');
858
+ expect((await push(repository)).pushed).toEqual([]);
859
+ });
860
+
861
+ it.each(['remote.pushDefault', 'branch.next.pushRemote'])('uses %s for first publication without an upstream', async (key) => {
862
+ const { repository, remote: origin } = createRepositoryWithRemote();
863
+ const fork = createTempDir();
864
+ runGit(fork, ['init', '--bare']);
865
+ runGit(repository, ['remote', 'add', 'fork', fork]);
866
+ runGit(repository, ['config', key, 'fork']);
867
+ runGit(repository, ['config', 'push.autoSetupRemote', 'false']);
868
+
869
+ expect((await push(repository)).pushed).toEqual([{ local: 'refs/heads/next', remote: 'fork' }]);
870
+ expect(readBranchConfig(repository, 'next', 'remote')).toBe('fork');
871
+ expect(() => runGit(origin, ['rev-parse', 'refs/heads/next'])).toThrow();
872
+ });
873
+
874
+ it('lets an explicit push remote override configured destinations', async () => {
875
+ const { repository, remote: origin } = createRepositoryWithRemote({ defaultBranch: 'next' });
876
+ const fork = createTempDir();
877
+ runGit(fork, ['init', '--bare']);
878
+ runGit(repository, ['remote', 'add', 'fork', fork]);
879
+ runGit(repository, ['branch', '--set-upstream-to=origin/next']);
880
+ runGit(repository, ['config', 'branch.next.pushRemote', 'fork']);
881
+ fs.appendFileSync(path.join(repository, 'README.md'), 'origin change\n');
882
+ runGit(repository, ['commit', '-am', 'Change for origin']);
883
+
884
+ expect((await push(repository, { remote: 'origin' })).pushed)
885
+ .toEqual([{ local: 'refs/heads/next', remote: 'origin' }]);
886
+ expect(runGit(origin, ['rev-parse', 'next']).trim()).toBe(runGit(repository, ['rev-parse', 'HEAD']).trim());
887
+ expect(() => runGit(fork, ['rev-parse', 'next'])).toThrow();
888
+ });
604
889
  });
605
890
 
606
891
  // ---------------------------------------------------------------------------
@@ -680,6 +965,78 @@ describe('getWorktrees', () => {
680
965
  expect(Array.isArray(result)).toBe(true);
681
966
  expect(warnSpy).not.toHaveBeenCalled();
682
967
  });
968
+ it('notifies subscribers only when another git process changes the worktree set', async () => {
969
+ if (!canRunGit()) return;
970
+
971
+ const repo = createTempDir();
972
+ runGit(repo, ['init', '-b', 'main']);
973
+ runGit(repo, ['config', 'user.email', 'test@example.com']);
974
+ runGit(repo, ['config', 'user.name', 'Test User']);
975
+ runGit(repo, ['commit', '--allow-empty', '-m', 'init']);
976
+ const worktreePath = path.join(createTempDir(), 'feature');
977
+
978
+ const events = [];
979
+ const unsubscribe = subscribeWorktreeTopologyChanges((event) => events.push(event));
980
+ try {
981
+ await observeWorktreeTopology(repo);
982
+ await observeWorktreeTopology(repo);
983
+ expect(events).toHaveLength(0);
984
+
985
+ runGit(repo, ['worktree', 'add', worktreePath, '-b', 'feature']);
986
+ await observeWorktreeTopology(worktreePath);
987
+ expect(events).toHaveLength(1);
988
+ expect(events[0].directories).toEqual(expect.arrayContaining([repo, worktreePath]));
989
+
990
+ await observeWorktreeTopology(repo);
991
+ expect(events).toHaveLength(1);
992
+
993
+ runGit(repo, ['worktree', 'remove', worktreePath]);
994
+ await observeWorktreeTopology(repo);
995
+ expect(events).toHaveLength(2);
996
+ } finally {
997
+ unsubscribe();
998
+ }
999
+ });
1000
+
1001
+ it('publishes worktrees this server creates and removes', async () => {
1002
+ if (!canRunGit()) return;
1003
+
1004
+ const previousXdgDataHome = process.env.XDG_DATA_HOME;
1005
+ process.env.XDG_DATA_HOME = createTempDir();
1006
+ const events = [];
1007
+ const unsubscribe = subscribeWorktreeTopologyChanges((event) => events.push(event));
1008
+ try {
1009
+ const repo = createTempDir();
1010
+ runGit(repo, ['init', '-b', 'main']);
1011
+ runGit(repo, ['config', 'user.email', 'test@example.com']);
1012
+ runGit(repo, ['config', 'user.name', 'Test User']);
1013
+ runGit(repo, ['commit', '--allow-empty', '-m', 'init']);
1014
+ await observeWorktreeTopology(repo);
1015
+
1016
+ const created = await createWorktree(repo, {
1017
+ mode: 'new',
1018
+ worktreeName: 'published',
1019
+ branchName: 'openchamber/published',
1020
+ });
1021
+ expect(events).toHaveLength(1);
1022
+ expect(events[0].directories).toContain(repo);
1023
+
1024
+ // The publish refreshed the baseline, so the next observation is quiet.
1025
+ await observeWorktreeTopology(repo);
1026
+ expect(events).toHaveLength(1);
1027
+
1028
+ await removeWorktree(repo, { directory: created.path });
1029
+ expect(events).toHaveLength(2);
1030
+ } finally {
1031
+ unsubscribe();
1032
+ if (previousXdgDataHome === undefined) {
1033
+ delete process.env.XDG_DATA_HOME;
1034
+ } else {
1035
+ process.env.XDG_DATA_HOME = previousXdgDataHome;
1036
+ }
1037
+ }
1038
+ });
1039
+
683
1040
  it('flags a worktree whose directory was deleted outside git as prunable', async () => {
684
1041
  const repo = createTempDir();
685
1042
  runGit(repo, ['init', '-b', 'main']);
@@ -2187,3 +2544,138 @@ describe.runIf(canRunGit())('getRangeFiles', () => {
2187
2544
  expect(copyEntry.path).toBe('copied destination.md');
2188
2545
  });
2189
2546
  });
2547
+
2548
+ // ---------------------------------------------------------------------------
2549
+ // getTrackingBranch
2550
+ // ---------------------------------------------------------------------------
2551
+
2552
+ describe('getTrackingBranch', () => {
2553
+ const createCommittedRepo = () => {
2554
+ const repo = createTempDir();
2555
+ runGit(repo, ['init', '-b', 'main']);
2556
+ runGit(repo, ['config', 'user.email', 'test@example.com']);
2557
+ runGit(repo, ['config', 'user.name', 'Test User']);
2558
+ runGit(repo, ['commit', '--allow-empty', '-m', 'Initial commit']);
2559
+ return repo;
2560
+ };
2561
+
2562
+ it('reports the same upstream name as status, including a gone upstream', async () => {
2563
+ if (!canRunGit()) return;
2564
+
2565
+ const repo = createCommittedRepo();
2566
+ await expect(getTrackingBranch(repo)).resolves.toBeNull();
2567
+
2568
+ runGit(repo, ['remote', 'add', 'origin', 'https://example.invalid/repo.git']);
2569
+ runGit(repo, ['config', 'branch.main.remote', 'origin']);
2570
+ runGit(repo, ['config', 'branch.main.merge', 'refs/heads/main']);
2571
+ await expect(getTrackingBranch(repo)).resolves.toBe('origin/main');
2572
+ expect((await getStatus(repo)).tracking).toBe('origin/main');
2573
+
2574
+ runGit(repo, ['update-ref', 'refs/remotes/origin/main', 'HEAD']);
2575
+ await expect(getTrackingBranch(repo)).resolves.toBe('origin/main');
2576
+ });
2577
+
2578
+ it('is null for a detached HEAD and outside a repository', async () => {
2579
+ if (!canRunGit()) return;
2580
+
2581
+ const repo = createCommittedRepo();
2582
+ runGit(repo, ['checkout', '--detach']);
2583
+ await expect(getTrackingBranch(repo)).resolves.toBeNull();
2584
+ await expect(getTrackingBranch(createTempDir())).resolves.toBeNull();
2585
+ });
2586
+ });
2587
+
2588
+ describe('getStatus concurrency', () => {
2589
+ it('answers overlapping reads of one repository and reflects changes made while a read ran', async () => {
2590
+ if (!canRunGit()) return;
2591
+
2592
+ const repo = createTempDir();
2593
+ runGit(repo, ['init', '-b', 'main']);
2594
+ runGit(repo, ['config', 'user.email', 'test@example.com']);
2595
+ runGit(repo, ['config', 'user.name', 'Test User']);
2596
+ runGit(repo, ['commit', '--allow-empty', '-m', 'Initial commit']);
2597
+
2598
+ const first = getStatus(repo);
2599
+ fs.writeFileSync(path.join(repo, 'late.txt'), 'added after the first read was admitted\n');
2600
+ const second = getStatus(repo, { mode: 'light' });
2601
+ const third = getStatus(repo);
2602
+
2603
+ const [firstStatus, secondStatus, thirdStatus] = await Promise.all([first, second, third]);
2604
+ expect(firstStatus.current).toBe('main');
2605
+ expect(secondStatus.files.map((file) => file.path)).toContain('late.txt');
2606
+ expect(thirdStatus.files.map((file) => file.path)).toContain('late.txt');
2607
+ // The follow-up run served both later callers at the widest requested mode.
2608
+ expect(secondStatus.diffStats).toBeDefined();
2609
+ expect(thirdStatus.diffStats).toBeDefined();
2610
+ });
2611
+ });
2612
+
2613
+ describe('getStatus untracked directories', () => {
2614
+ const callDiffRoute = async (endpoint, query) => {
2615
+ const routes = new Map();
2616
+ registerGitRoutes({ get: (url, handler) => routes.set(url, handler), post() {}, put() {}, delete() {} });
2617
+ let status = 200;
2618
+ let body;
2619
+ await routes.get(`/api/git/${endpoint}`)({ query }, {
2620
+ status(value) { status = value; return this; },
2621
+ json(value) { body = value; },
2622
+ });
2623
+ return { status, body };
2624
+ };
2625
+
2626
+ const createCommittedRepo = () => {
2627
+ const repo = createTempDir();
2628
+ runGit(repo, ['init', '-b', 'main']);
2629
+ runGit(repo, ['config', 'user.email', 'test@example.com']);
2630
+ runGit(repo, ['config', 'user.name', 'Test User']);
2631
+ fs.writeFileSync(path.join(repo, 'README.md'), '# Test\n');
2632
+ runGit(repo, ['add', 'README.md']);
2633
+ runGit(repo, ['commit', '-m', 'Initial commit']);
2634
+ return repo;
2635
+ };
2636
+
2637
+ const writeFiles = (root, count) => {
2638
+ fs.mkdirSync(root, { recursive: true });
2639
+ for (let index = 0; index < count; index += 1) {
2640
+ fs.writeFileSync(path.join(root, `file-${String(index).padStart(5, '0')}.txt`), `${index}\n`);
2641
+ }
2642
+ };
2643
+
2644
+ it('lists the files of an ordinary new directory one by one', async () => {
2645
+ if (!canRunGit()) return;
2646
+
2647
+ const repo = createCommittedRepo();
2648
+ writeFiles(path.join(repo, 'feature', 'deep'), 3);
2649
+ fs.writeFileSync(path.join(repo, 'loose.txt'), 'loose\n');
2650
+
2651
+ const paths = (await getStatus(repo)).files.map((file) => file.path);
2652
+ expect(paths).toEqual([
2653
+ 'feature/deep/file-00000.txt',
2654
+ 'feature/deep/file-00001.txt',
2655
+ 'feature/deep/file-00002.txt',
2656
+ 'loose.txt',
2657
+ ]);
2658
+ });
2659
+
2660
+ it('keeps a directory with more than a thousand new files as one entry the diff routes explain', async () => {
2661
+ if (!canRunGit()) return;
2662
+
2663
+ const repo = createCommittedRepo();
2664
+ writeFiles(path.join(repo, 'node_modules', 'pkg'), 1001);
2665
+ writeFiles(path.join(repo, 'small'), 2);
2666
+
2667
+ const status = await getStatus(repo);
2668
+ expect(status.files.map((file) => file.path)).toEqual([
2669
+ 'node_modules/',
2670
+ 'small/file-00000.txt',
2671
+ 'small/file-00001.txt',
2672
+ ]);
2673
+ expect(status.files[0]).toMatchObject({ index: '?', working_dir: '?' });
2674
+
2675
+ for (const endpoint of ['diff', 'file-diff']) {
2676
+ const { status: httpStatus, body } = await callDiffRoute(endpoint, { directory: repo, path: 'node_modules/' });
2677
+ expect(httpStatus).toBe(422);
2678
+ expect(body).toEqual({ code: 'untracked_directory', error: 'Path is a directory of untracked files: node_modules/' });
2679
+ }
2680
+ });
2681
+ });
@@ -63,6 +63,11 @@
63
63
 
64
64
  ## Consumers of PR data
65
65
 
66
+ PR list/search failures propagate as errors instead of successful empty lists.
67
+ Comparison pickers use these responses to offer retry rather than claiming the
68
+ repository has no PRs. A failed repository in a multi-repository listing fails
69
+ that page, so callers cannot mistake a partial page for a complete one.
70
+
66
71
  - `packages/ui/src/components/session/SessionSidebar.tsx` reads all PR entries and maps them to `directory::branch`.
67
72
  - `packages/ui/src/components/session/sidebar/SessionGroupSection.tsx` renders the compact badge, PR number, title, checks summary, and GitHub link.
68
73
  - `packages/ui/src/components/views/git/PullRequestSection.tsx` uses the same shared entry for the full PR workflow.
@@ -1,5 +1,5 @@
1
1
  import { stat } from 'node:fs/promises';
2
- import { getRemotes, getStatus } from '../git/index.js';
2
+ import { getRemotes, getTrackingBranch } from '../git/index.js';
3
3
  import { resolveGitHubRepoFromDirectory } from './repo/index.js';
4
4
  import { noteIfGitHubRateLimit } from './rate-limit.js';
5
5
 
@@ -644,13 +644,13 @@ export async function resolveGitHubPrStatus({ octokit, directory, branch, remote
644
644
  const normalizedBranch = normalizeText(branch);
645
645
  const normalizedRemoteName = normalizeText(remoteName) || 'origin';
646
646
 
647
- const [status, remotes] = await Promise.all([
648
- getStatus(directory).catch(() => null),
647
+ const [tracking, remotes] = await Promise.all([
648
+ getTrackingBranch(directory).catch(() => null),
649
649
  getRemotes(directory).catch(() => []),
650
650
  ]);
651
651
 
652
- const trackingRemoteName = parseTrackingRemoteName(status?.tracking);
653
- const trackingBranchName = parseTrackingBranchName(status?.tracking);
652
+ const trackingRemoteName = parseTrackingRemoteName(tracking);
653
+ const trackingBranchName = parseTrackingBranchName(tracking);
654
654
  const branchCandidates = [];
655
655
  pushUnique(branchCandidates, normalizedBranch);
656
656
  pushUnique(branchCandidates, trackingBranchName);
@@ -4,7 +4,7 @@ const listMock = mock(async () => ({ data: [] }));
4
4
 
5
5
  mock.module('../git/index.js', () => ({
6
6
  getRemotes: async () => [],
7
- getStatus: async () => null,
7
+ getTrackingBranch: async () => null,
8
8
  }));
9
9
 
10
10
  mock.module('./repo/index.js', () => ({
@@ -781,14 +781,14 @@ export function registerGitHubRoutes(app) {
781
781
  // Determine the source remote for the head branch
782
782
  // Priority: 1) explicit headRemote, 2) tracking branch remote, 3) 'origin' if targeting non-origin
783
783
  let sourceRemote = headRemote;
784
- const { getStatus, getRemotes } = await import('../git/index.js');
784
+ const { getTrackingBranch, getRemotes } = await import('../git/index.js');
785
785
 
786
786
  // If no explicit headRemote, check the branch's tracking info
787
787
  if (!sourceRemote) {
788
- const status = await getStatus(directory).catch(() => null);
789
- if (status?.tracking) {
788
+ const tracking = await getTrackingBranch(directory).catch(() => null);
789
+ if (tracking) {
790
790
  // tracking is like "gsxdsm/fix/multi-remote-branch-creation" or "origin/main"
791
- const trackingRemote = status.tracking.split('/')[0];
791
+ const trackingRemote = tracking.split('/')[0];
792
792
  if (trackingRemote) {
793
793
  sourceRemote = trackingRemote;
794
794
  }
@@ -1513,7 +1513,7 @@ export function registerGitHubRoutes(app) {
1513
1513
  return res.json({ connected: true, repo, prs, page: effectivePage, hasMore });
1514
1514
  } catch (error) {
1515
1515
  console.error('Failed to search GitHub PRs:', error);
1516
- return res.json({ connected: true, repo, prs: [], page: effectivePage, hasMore: false });
1516
+ throw error;
1517
1517
  }
1518
1518
  }
1519
1519
 
@@ -1532,7 +1532,7 @@ export function registerGitHubRoutes(app) {
1532
1532
  return { prs, hasMore };
1533
1533
  } catch (error) {
1534
1534
  console.warn(`Failed to list PRs for ${repoRef.owner}/${repoRef.repo}:`, error?.message || error);
1535
- return { prs: [], hasMore: false };
1535
+ throw error;
1536
1536
  }
1537
1537
  };
1538
1538