@vantran-se/openchamber-web 1.23.1 → 1.24.4

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 (313) 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-BOpqXpFU.js → CodeMirrorCommentWidgets-B4a0JkUz.js} +1 -1
  5. package/dist/assets/DiffView-B2CD9wC8.js +4 -0
  6. package/dist/assets/FilesView-C4vMFclc.js +13 -0
  7. package/dist/assets/GitView-Byu2DY7s.js +6 -0
  8. package/dist/assets/GuestAttachDialog-B808E0-d.js +1 -0
  9. package/dist/assets/{JsonTreeView-nd_E8eVM.js → JsonTreeView-VW_7czo3.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-EzGNCGrd.js +1 -0
  70. package/dist/assets/{MarkdownImageGallery-CVYds3sQ.js → MarkdownImageGallery-B2YUDFaB.js} +1 -1
  71. package/dist/assets/MarkdownRendererImpl-B4ikIzR3.js +12 -0
  72. package/dist/assets/{OnboardingScreen-Dd2IbPTn.js → OnboardingScreen-CQHBSTEi.js} +1 -1
  73. package/dist/assets/{PatchDiff-nUu5i4bG.js → PatchDiff-DCjGhlYV.js} +1 -1
  74. package/dist/assets/PierreDiffViewer-CEDZNkU2.js +210 -0
  75. package/dist/assets/PlanView-D3x-pTkZ.js +5 -0
  76. package/dist/assets/PluginPane-DEcgDQ9h.js +3 -0
  77. package/dist/assets/SettingsView-CSinuH-1.js +29 -0
  78. package/dist/assets/{SettingsWindow-BccxY81J.js → SettingsWindow-B5CHoJFZ.js} +1 -1
  79. package/dist/assets/SubmoduleDiffSummary-oxeACCWs.js +1 -0
  80. package/dist/assets/ToolOutputDialog-tLNWmfcW.js +15 -0
  81. package/dist/assets/{ToolPartDiffPreview-BFVgk0UK.js → ToolPartDiffPreview-CflzmK-x.js} +2 -2
  82. package/dist/assets/{VirtualizedFileDiff-z3_H74UR.js → VirtualizedFileDiff-NU0FHFhT.js} +3 -3
  83. package/dist/assets/{WalkthroughView-BqM9GFbJ.js → WalkthroughView-BZyjhqHv.js} +2 -2
  84. package/dist/assets/appThemeRegistry-DLicw4dH.js +1 -0
  85. package/dist/assets/commitAndPush-BDftrjr6.js +1 -0
  86. package/dist/assets/{computeVirtualFileMetrics-Bdazp9Qo.js → computeVirtualFileMetrics-DlWlEM3x.js} +1 -1
  87. package/dist/assets/de-Bro6_zyl.js +10 -0
  88. package/dist/assets/es-Bb9XQZvd.js +16 -0
  89. package/dist/assets/fr-Dgfzn-IL.js +14 -0
  90. package/dist/assets/{index-eo1-D_7z.js → index-CtQNmqUL.js} +1 -1
  91. package/dist/assets/index-DqaMWNrC.css +1 -0
  92. package/dist/assets/index-Dqc6USl3.js +40 -0
  93. package/dist/assets/{isDiffPlainText-BvKH712-.js → isDiffPlainText-CO4e5J_S.js} +1 -1
  94. package/dist/assets/{isFilePlainText-BdVVPukf.js → isFilePlainText-ue6kCou7.js} +1 -1
  95. package/dist/assets/ja-BYsakGFh.js +16 -0
  96. package/dist/assets/ko-BSaQz9aN.js +16 -0
  97. package/dist/assets/main-B62_pjL5.js +2 -0
  98. package/dist/assets/main-BEdcOI2P.js +896 -0
  99. package/dist/assets/{markdown-shiki.worker-D5fBU-lw.js → markdown-shiki.worker-CahLvHIq.js} +5 -5
  100. package/dist/assets/markdownCore-CK48LG0z.js +5 -0
  101. package/dist/assets/miniChat-LT8Rqofr.js +2 -0
  102. package/dist/assets/mobile-CqarEynh.js +2 -0
  103. package/dist/assets/{patchFileDiff-B7LjppSS.js → patchFileDiff-D7QVlovD.js} +3 -3
  104. package/dist/assets/pl-OIviPdoj.js +16 -0
  105. package/dist/assets/pt-BR-Cltg-E6s.js +16 -0
  106. package/dist/assets/renderElectronMiniChatApp-COyLTtZx.js +2 -0
  107. package/dist/assets/renderMobileApp-BC1MHg6g.js +25 -0
  108. package/dist/assets/runtimeEndpointReset-DdrazOVH.js +25 -0
  109. package/dist/assets/{scheduledTasksApi-hceovDei.js → scheduledTasksApi-B2SWMGEU.js} +1 -1
  110. package/dist/assets/shikiHighlight-BPu3JiQs.js +1 -0
  111. package/dist/assets/textMateThemeFromAppTheme-DWqRPo9A.js +1 -0
  112. package/dist/assets/tr-CQI-KPre.js +7 -0
  113. package/dist/assets/uk-CqW7R4DA.js +16 -0
  114. package/dist/assets/useAppFontEffects-ne84JBVL.js +7333 -0
  115. package/dist/assets/useInlineCommentController-CgS0fnw_.js +2 -0
  116. package/dist/assets/useRootScrollLock-Cpf1qoGp.js +2 -0
  117. package/dist/assets/{vendor-base-ui-BY_5qla_.js → vendor-base-ui-CsXYnR5c.js} +1 -1
  118. package/dist/assets/{vendor-codemirror-language-data-DWA1bm1F.js → vendor-codemirror-language-data-Bx2EHc1O.js} +2 -2
  119. package/dist/assets/vendor-framer-motion-Bq3iTZ6r.js +1 -0
  120. package/dist/assets/vendor-katex-Cp579UoJ.css +1 -0
  121. package/dist/assets/{vendor-motion-dom-D_o3BTI3.js → vendor-motion-dom-DcCgeG69.js} +1 -1
  122. package/dist/assets/{vendor-remixicon-react-Dil0zKKT.js → vendor-remixicon-react-BDmGmFCA.js} +1 -1
  123. package/dist/assets/vendor-strip-json-comments-5dJlOkP1.js +4 -0
  124. package/dist/assets/{vendor-tanstack-react-virtual-g05Cx3z9.js → vendor-tanstack-react-virtual-ClYN8CzB.js} +1 -1
  125. package/dist/assets/{vendor-tanstack-virtual-core-Dubg10g5.js → vendor-tanstack-virtual-core-BewVQryB.js} +1 -1
  126. package/dist/assets/{vendor-zod-B6551dwG.js → vendor-zod-CgAdXWZO.js} +4 -4
  127. package/dist/assets/{web-components-B_Z64So5.js → web-components-DxX8bZ1o.js} +2 -2
  128. package/dist/assets/zh-CN-CVZG_o_J.js +16 -0
  129. package/dist/assets/zh-TW-D5ZxzCZp.js +16 -0
  130. package/dist/index.html +6 -4
  131. package/dist/mini-chat.html +6 -4
  132. package/dist/mobile.html +6 -4
  133. package/package.json +12 -9
  134. package/server/built-in-extensions/registry.json +4 -0
  135. package/server/index.d.ts +2 -0
  136. package/server/index.js +44 -10
  137. package/server/lib/agent-tool/DOCUMENTATION.md +17 -4
  138. package/server/lib/agent-tool/runtime.js +62 -15
  139. package/server/lib/agent-tool/runtime.test.js +88 -2
  140. package/server/lib/changelog/update-notes.js +2 -1
  141. package/server/lib/changelog/update-notes.test.js +19 -0
  142. package/server/lib/event-stream/DOCUMENTATION.md +23 -0
  143. package/server/lib/event-stream/delta-coalescer.js +0 -0
  144. package/server/lib/event-stream/delta-coalescer.test.js +292 -0
  145. package/server/lib/event-stream/global-hub.js +53 -23
  146. package/server/lib/event-stream/global-hub.test.js +160 -0
  147. package/server/lib/event-stream/global-ws-bridge.js +1 -0
  148. package/server/lib/event-stream/index.js +4 -0
  149. package/server/lib/fs/DOCUMENTATION.md +10 -1
  150. package/server/lib/fs/git-process.test.js +77 -0
  151. package/server/lib/fs/routes.js +58 -5
  152. package/server/lib/fs/routes.test.js +116 -0
  153. package/server/lib/fs/search.js +2 -1
  154. package/server/lib/git/DOCUMENTATION.md +20 -5
  155. package/server/lib/git/routes.js +16 -3
  156. package/server/lib/git/serial-refresh.js +93 -0
  157. package/server/lib/git/serial-refresh.test.js +112 -0
  158. package/server/lib/git/service.js +429 -70
  159. package/server/lib/git/service.test.js +418 -0
  160. package/server/lib/github/pr-status.js +5 -5
  161. package/server/lib/github/pr-status.test.js +1 -1
  162. package/server/lib/github/routes.js +4 -4
  163. package/server/lib/guests/DOCUMENTATION.md +68 -0
  164. package/server/lib/guests/auth-store.js +184 -0
  165. package/server/lib/guests/auth-store.test.js +112 -0
  166. package/server/lib/guests/background.test.js +83 -0
  167. package/server/lib/guests/builtins.js +34 -0
  168. package/server/lib/guests/builtins.test.js +174 -0
  169. package/server/lib/guests/catalog.js +477 -0
  170. package/server/lib/guests/catalog.test.js +390 -0
  171. package/server/lib/guests/clone.js +297 -0
  172. package/server/lib/guests/extract-zip.js +136 -0
  173. package/server/lib/guests/extract-zip.test.js +109 -0
  174. package/server/lib/guests/files.js +308 -0
  175. package/server/lib/guests/files.test.js +262 -0
  176. package/server/lib/guests/grant-scope.js +81 -0
  177. package/server/lib/guests/grant-scope.test.js +75 -0
  178. package/server/lib/guests/host-session.js +73 -0
  179. package/server/lib/guests/host-session.test.js +30 -0
  180. package/server/lib/guests/html-styles.js +6 -0
  181. package/server/lib/guests/html-styles.test.js +45 -0
  182. package/server/lib/guests/html-tokens.js +73 -0
  183. package/server/lib/guests/html-tokens.test.js +53 -0
  184. package/server/lib/guests/install.js +406 -0
  185. package/server/lib/guests/install.test.js +338 -0
  186. package/server/lib/guests/oauth.js +501 -0
  187. package/server/lib/guests/oauth.test.js +481 -0
  188. package/server/lib/guests/persist.js +336 -0
  189. package/server/lib/guests/persist.test.js +288 -0
  190. package/server/lib/guests/request.js +112 -0
  191. package/server/lib/guests/request.test.js +206 -0
  192. package/server/lib/guests/routes.js +838 -0
  193. package/server/lib/guests/service.js +538 -0
  194. package/server/lib/guests/service.test.js +277 -0
  195. package/server/lib/guests/sockets.js +129 -0
  196. package/server/lib/guests/sockets.test.js +110 -0
  197. package/server/lib/guests/ssh-install.test.js +188 -0
  198. package/server/lib/guests/storage.js +68 -0
  199. package/server/lib/guests/storage.test.js +59 -0
  200. package/server/lib/guests/updates.js +294 -0
  201. package/server/lib/guests/updates.test.js +334 -0
  202. package/server/lib/guests/upload.js +50 -0
  203. package/server/lib/guests/upload.test.js +131 -0
  204. package/server/lib/linear/DOCUMENTATION.md +1 -1
  205. package/server/lib/message-queue/DOCUMENTATION.md +10 -1
  206. package/server/lib/message-queue/runtime.js +21 -1
  207. package/server/lib/message-queue/runtime.test.js +52 -5
  208. package/server/lib/notifications/routes.js +5 -0
  209. package/server/lib/openchamber-control/DOCUMENTATION.md +6 -0
  210. package/server/lib/openchamber-sessions/routes.js +62 -29
  211. package/server/lib/openchamber-sessions/routes.test.js +96 -0
  212. package/server/lib/opencode/DOCUMENTATION.md +55 -1
  213. package/server/lib/opencode/bootstrap-runtime.js +1 -0
  214. package/server/lib/opencode/core-routes.js +39 -1
  215. package/server/lib/opencode/feature-routes-runtime.js +15 -0
  216. package/server/lib/opencode/lifecycle-process.test.js +84 -0
  217. package/server/lib/opencode/lifecycle.js +54 -67
  218. package/server/lib/opencode/openchamber-routes.js +72 -14
  219. package/server/lib/opencode/openchamber-routes.test.js +71 -3
  220. package/server/lib/opencode/session-runtime.js +68 -0
  221. package/server/lib/opencode/session-runtime.test.js +43 -0
  222. package/server/lib/opencode/settings-helpers.js +8 -0
  223. package/server/lib/opencode/settings-helpers.test.js +13 -1
  224. package/server/lib/opencode/settings-normalization-runtime.js +2 -0
  225. package/server/lib/opencode/settings-normalization-runtime.test.js +13 -0
  226. package/server/lib/opencode/settings-registry.json +15 -0
  227. package/server/lib/opencode/shutdown-runtime.js +3 -0
  228. package/server/lib/opencode/shutdown-runtime.test.js +24 -0
  229. package/server/lib/opencode/theme-archive.js +53 -0
  230. package/server/lib/opencode/theme-catalog.js +171 -0
  231. package/server/lib/opencode/theme-catalog.test.js +125 -0
  232. package/server/lib/opencode/theme-import.test.js +144 -0
  233. package/server/lib/opencode/theme-runtime.js +100 -28
  234. package/server/lib/opencode/theme-runtime.test.js +15 -0
  235. package/server/lib/permission-auto-accept/DOCUMENTATION.md +8 -0
  236. package/server/lib/permission-auto-accept/runtime.js +13 -0
  237. package/server/lib/permission-auto-accept/runtime.test.js +37 -1
  238. package/server/lib/projects/project-config.js +2 -2
  239. package/server/lib/quota/DOCUMENTATION.md +0 -1
  240. package/server/lib/quota/providers/index.js +0 -7
  241. package/server/lib/realtime-proxy-terminal.test.js +150 -0
  242. package/server/lib/realtime-proxy.js +2 -0
  243. package/server/lib/routing/DOCUMENTATION.md +83 -0
  244. package/server/lib/routing/defaults.js +98 -0
  245. package/server/lib/routing/feature-flag.js +11 -0
  246. package/server/lib/routing/history.js +34 -0
  247. package/server/lib/routing/jev.js +98 -0
  248. package/server/lib/routing/routes.http.test.js +100 -0
  249. package/server/lib/routing/routes.js +81 -0
  250. package/server/lib/routing/runtime.js +217 -0
  251. package/server/lib/routing/runtime.test.js +188 -0
  252. package/server/lib/routing/store.js +249 -0
  253. package/server/lib/routing/store.test.js +96 -0
  254. package/server/lib/scheduled-tasks/runtime.js +2 -2
  255. package/server/lib/scheduled-tasks/runtime.test.js +14 -0
  256. package/server/lib/security/request-security.js +9 -5
  257. package/server/lib/security/request-security.test.js +37 -0
  258. package/server/lib/small-model/DOCUMENTATION.md +14 -2
  259. package/server/lib/small-model/call.js +37 -6
  260. package/server/lib/small-model/call.test.js +182 -0
  261. package/server/lib/terminal/DOCUMENTATION.md +23 -2
  262. package/server/lib/terminal/runtime.js +27 -5
  263. package/server/lib/terminal/runtime.test.js +67 -0
  264. package/server/lib/terminal/shutdown.js +106 -0
  265. package/server/lib/terminal/shutdown.test.js +84 -0
  266. package/server/lib/tts/service.test.js +78 -0
  267. package/server/lib/ui-auth/DOCUMENTATION.md +8 -0
  268. package/server/lib/ui-auth/session-cookie.js +51 -0
  269. package/server/lib/ui-auth/session-cookie.test.js +46 -0
  270. package/server/lib/ui-auth/ui-auth.js +56 -17
  271. package/server/lib/ui-auth/ui-auth.test.js +96 -0
  272. package/dist/assets/DiffView-CyLmb-wF.js +0 -4
  273. package/dist/assets/DirtyBranchSwitchDialog-pu5kcoir.js +0 -1
  274. package/dist/assets/FilesView-WtJB0y_l.js +0 -13
  275. package/dist/assets/GitView-DkNrV0BK.js +0 -6
  276. package/dist/assets/LinearIssuesView-B0H0Otii.js +0 -1
  277. package/dist/assets/MarkdownRendererImpl-DSqVN2d8.js +0 -12
  278. package/dist/assets/PierreDiffViewer-Bsq9z-sw.js +0 -161
  279. package/dist/assets/PlanView-CAFziKH5.js +0 -5
  280. package/dist/assets/SettingsView-BRittavZ.js +0 -28
  281. package/dist/assets/ToolOutputDialog-B0uYaJio.js +0 -15
  282. package/dist/assets/appThemeRegistry-BK0dHKa6.js +0 -1
  283. package/dist/assets/de-c7CYLCAW.js +0 -10
  284. package/dist/assets/es-BKAVPQmQ.js +0 -16
  285. package/dist/assets/fr-Bj1glqfV.js +0 -14
  286. package/dist/assets/index-BYFyOu_D.css +0 -1
  287. package/dist/assets/index-BfwRClDe.js +0 -40
  288. package/dist/assets/ja-5jwSIbK0.js +0 -16
  289. package/dist/assets/ko-BaDbg-Eb.js +0 -16
  290. package/dist/assets/main-CGG2_uHq.js +0 -2
  291. package/dist/assets/main-CcrSaL4J.js +0 -930
  292. package/dist/assets/markdownCore-DsmQr3bt.js +0 -5
  293. package/dist/assets/miniChat-DEaAhP4b.js +0 -2
  294. package/dist/assets/mobile-BC3rsvv0.js +0 -2
  295. package/dist/assets/pl-BFKVBDFu.js +0 -16
  296. package/dist/assets/pt-BR-qKfHvkDL.js +0 -16
  297. package/dist/assets/registerCustomTheme-BBXUCRnG.js +0 -1
  298. package/dist/assets/renderElectronMiniChatApp-CR9d1Mr0.js +0 -2
  299. package/dist/assets/renderMobileApp-Da5dwobZ.js +0 -25
  300. package/dist/assets/runtimeEndpointReset-CLYVRw2H.js +0 -41
  301. package/dist/assets/shikiHighlight-D1Z9G58W.js +0 -1
  302. package/dist/assets/tr-DYXPoEDG.js +0 -7
  303. package/dist/assets/uk-CwAoloR8.js +0 -16
  304. package/dist/assets/useAppFontEffects-DeStGcUT.js +0 -7282
  305. package/dist/assets/useGitComparison-CpxioduU.js +0 -1
  306. package/dist/assets/useInlineCommentController-gmUnJtrb.js +0 -2
  307. package/dist/assets/useRootScrollLock--a3cPz8s.js +0 -1
  308. package/dist/assets/vendor-framer-motion-livSKyyV.js +0 -1
  309. package/dist/assets/zh-CN-D9MG0lqF.js +0 -16
  310. package/dist/assets/zh-TW-1HNK7shh.js +0 -16
  311. package/server/lib/quota/providers/crof.js +0 -96
  312. package/server/lib/quota/providers/crof.test.js +0 -98
  313. /package/dist/assets/{vendor-katex-k3Ppq1Ha.js → vendor-katex-CHD9BdDO.js} +0 -0
@@ -1,8 +1,9 @@
1
1
  import simpleGit from 'simple-git';
2
+ import { createSerialRefresh } from './serial-refresh.js';
2
3
  import fs from 'fs';
3
4
  import path from 'path';
4
5
  import os from 'os';
5
- import { execFile } from 'child_process';
6
+ import { execFile, spawn } from 'child_process';
6
7
  import { promisify } from 'util';
7
8
  import { createRequire } from 'module';
8
9
 
@@ -349,12 +350,22 @@ const buildGitEnv = async () => {
349
350
  env.SSH_AUTH_SOCK = resolved;
350
351
  }
351
352
  }
353
+ // The server has no terminal a user could answer. Without this, Git asks
354
+ // for a username or password on its (hidden, on Windows) console and waits
355
+ // forever; credential helpers and GUI prompts still run before this point.
356
+ if (env.GIT_TERMINAL_PROMPT === undefined) {
357
+ env.GIT_TERMINAL_PROMPT = '0';
358
+ }
352
359
  return env;
353
360
  };
354
361
 
355
- const createGit = async (directory, { allowUnsafeSshCommand = false, allowUnsafeCredentialHelper = false } = {}) => {
362
+ const createGit = async (directory, { allowUnsafeSshCommand = false, allowUnsafeCredentialHelper = false, stallTimeoutMs = 0 } = {}) => {
356
363
  const env = await buildGitEnv();
357
364
  const spawnOptions = { windowsHide: true };
365
+ // simple-git's block timeout kills the process once it has produced no
366
+ // output for this long. Opt-in per caller: a background read must never hold
367
+ // a limiter slot forever, while a silent long push or fetch must not be cut.
368
+ const timeout = stallTimeoutMs > 0 ? { block: stallTimeoutMs } : undefined;
358
369
  const binary = getGitBinary();
359
370
  const hasCustomBinary = typeof binary === 'string' && binary.trim() && binary !== 'git' && binary !== 'git.exe';
360
371
  const unsafe = hasCustomBinary || allowUnsafeSshCommand || allowUnsafeCredentialHelper
@@ -379,6 +390,7 @@ const createGit = async (directory, { allowUnsafeSshCommand = false, allowUnsafe
379
390
  spawnOptions,
380
391
  binary,
381
392
  unsafe,
393
+ ...(timeout ? { timeout } : {}),
382
394
  });
383
395
  };
384
396
 
@@ -484,14 +496,14 @@ const resolveGitRepositoryRoot = async (directoryPath, git) => {
484
496
  : path.resolve(directoryPath, normalizedTopLevel);
485
497
  };
486
498
 
487
- const createRepositoryGitContext = async (directory) => {
499
+ const createRepositoryGitContext = async (directory, gitOptions = {}) => {
488
500
  const directoryPath = normalizeDirectoryPath(directory);
489
501
  if (typeof directoryPath !== 'string' || !directoryPath.trim()) {
490
502
  throw new Error('Git directory is required');
491
503
  }
492
- const directoryGit = await createGit(directoryPath);
504
+ const directoryGit = await createGit(directoryPath, gitOptions);
493
505
  const repoRoot = await resolveGitRepositoryRoot(directoryPath, directoryGit);
494
- const git = path.resolve(directoryPath) === repoRoot ? directoryGit : await createGit(repoRoot);
506
+ const git = path.resolve(directoryPath) === repoRoot ? directoryGit : await createGit(repoRoot, gitOptions);
495
507
  return { directoryPath, directoryGit, repoRoot, git };
496
508
  };
497
509
 
@@ -510,12 +522,46 @@ const resolveGitInternalPath = async (repoRoot, git, gitPath) => {
510
522
  return path.resolve(repoRoot, resolved.trim());
511
523
  };
512
524
 
525
+ const GITLINK_MODE = '160000';
526
+
527
+ // Paths from `git status` can stop resolving: the file was removed after the
528
+ // listing, or the entry is a nested repository git reports as `dir/`. Callers
529
+ // tell these apart by `code`, and diff routes send the code to clients as is.
530
+ const GIT_PATH_NOT_FOUND = 'path_not_found';
531
+ const GIT_PATH_IS_NESTED_REPOSITORY = 'nested_repository';
532
+ const GIT_PATH_IS_UNTRACKED_DIRECTORY = 'untracked_directory';
533
+
534
+ const GIT_PATH_ERROR_MESSAGES = {
535
+ [GIT_PATH_IS_NESTED_REPOSITORY]: (filePath) => `Path is a separate Git repository: ${filePath}`,
536
+ [GIT_PATH_IS_UNTRACKED_DIRECTORY]: (filePath) => `Path is a directory of untracked files: ${filePath}`,
537
+ [GIT_PATH_NOT_FOUND]: (filePath) => `Path not found in working tree, index, or HEAD: ${filePath}`,
538
+ };
539
+
540
+ const createGitPathError = (code, filePath) => Object.assign(new Error(GIT_PATH_ERROR_MESSAGES[code](filePath)), { code });
541
+
542
+ // Mode of the exact entry at `repoPath`, or null. `cat-file -e` cannot answer
543
+ // this: a gitlink's commit lives in the submodule's object store, so git exits 1
544
+ // without stderr, which simple-git reports as success.
545
+ const readGitEntryMode = async (repoRoot, args, repoPath) => {
546
+ const result = await runGitCommand(repoRoot, args);
547
+ if (!result.success) return null;
548
+ for (const record of result.stdout.split('\0')) {
549
+ const tab = record.indexOf('\t');
550
+ if (tab !== -1 && record.slice(tab + 1) === repoPath) {
551
+ return record.slice(0, record.indexOf(' '));
552
+ }
553
+ }
554
+ return null;
555
+ };
556
+
513
557
  const resolveGitFileContext = async (directoryPath, git, filePath, repoRootOverride = null) => {
514
558
  const repoRoot = repoRootOverride || await resolveGitRepositoryRoot(directoryPath, git);
515
559
  const candidates = Array.from(new Set([
516
560
  path.resolve(repoRoot, filePath),
517
561
  path.resolve(directoryPath, filePath),
518
562
  ]));
563
+ let nestedRepository = false;
564
+ let untrackedDirectory = false;
519
565
 
520
566
  for (const absolutePath of candidates) {
521
567
  if (!isInsideOrSameDirectory(repoRoot, absolutePath)) {
@@ -526,20 +572,68 @@ const resolveGitFileContext = async (directoryPath, git, filePath, repoRootOverr
526
572
  const worktreeEntry = await fsp.lstat(absolutePath).catch(() => null);
527
573
  const isSymbolicLink = worktreeEntry?.isSymbolicLink() ?? false;
528
574
  const existsInWorktree = worktreeEntry?.isFile() || isSymbolicLink;
529
- const existsInIndex = await git.raw(['cat-file', '-e', `:${repoPath}`]).then(() => true).catch(() => false);
530
- const existsInHead = await git.raw(['cat-file', '-e', `HEAD:${repoPath}`]).then(() => true).catch(() => false);
575
+ const indexMode = await readGitEntryMode(repoRoot, ['ls-files', '--stage', '-z', '--', `:(literal)${repoPath}`], repoPath);
576
+ const headMode = await readGitEntryMode(repoRoot, ['ls-tree', '-z', 'HEAD', '--', repoPath], repoPath);
531
577
 
532
- if (existsInWorktree || existsInIndex || existsInHead) {
578
+ if (existsInWorktree || indexMode || headMode) {
533
579
  return {
534
580
  absolutePath,
535
581
  repoPath,
536
582
  repoRoot,
537
583
  isSymbolicLink,
584
+ isSubmodule: indexMode === GITLINK_MODE || headMode === GITLINK_MODE,
538
585
  };
539
586
  }
587
+
588
+ if (worktreeEntry?.isDirectory()) {
589
+ if (await fsp.lstat(path.join(absolutePath, '.git')).then(() => true, () => false)) {
590
+ nestedRepository = true;
591
+ } else {
592
+ // Status lists a directory whose untracked files were not expanded
593
+ // (see readStatus) as `dir/`; there is no single patch for it.
594
+ untrackedDirectory = true;
595
+ }
596
+ }
540
597
  }
541
598
 
542
- throw new Error('Invalid file path');
599
+ if (nestedRepository) throw createGitPathError(GIT_PATH_IS_NESTED_REPOSITORY, filePath);
600
+ if (untrackedDirectory) throw createGitPathError(GIT_PATH_IS_UNTRACKED_DIRECTORY, filePath);
601
+ throw createGitPathError(GIT_PATH_NOT_FOUND, filePath);
602
+ };
603
+
604
+ /**
605
+ * What a submodule entry records, since its text patch cannot show everything:
606
+ * with only untracked files inside, `git status` marks it modified while
607
+ * `git diff` prints nothing.
608
+ */
609
+ const readSubmoduleState = async (repoRoot, fileContext) => {
610
+ const status = await runGitCommand(repoRoot, ['status', '--porcelain=v2', '-z', '--', `:(literal)${fileContext.repoPath}`]);
611
+ if (!status.success) {
612
+ throw new Error(status.message || 'Failed to read submodule status');
613
+ }
614
+ // Changed: "1 XY S<c><m><u> mH mI mW hH hI path" ("2" adds rename fields
615
+ // after hI). Unmerged: "u XY S<c><m><u> m1 m2 m3 mW h1 h2 h3 path", with no
616
+ // stage-0 index entry. A clean submodule has no record, so HEAD and the index
617
+ // record the same commit.
618
+ const record = status.stdout.split('\0').find((entry) => /^[12u] /.test(entry))?.split(' ');
619
+ const hasConflict = record?.[0] === 'u';
620
+ const readHead = async () => (await runGitCommand(repoRoot, ['rev-parse', '--verify', '--quiet', `HEAD:${fileContext.repoPath}`])).stdout.trim();
621
+ const head = record && !hasConflict ? record[6] : await readHead();
622
+ const index = hasConflict ? '' : (record ? record[7] : head);
623
+ const flags = record ? record[2] : 'S...';
624
+ // Without its own `.git`, rev-parse would answer for the parent repository.
625
+ const initialized = await fsp.lstat(path.join(fileContext.absolutePath, '.git')).then(() => true, () => false);
626
+ const worktree = initialized ? await runGitCommand(fileContext.absolutePath, ['rev-parse', '--verify', 'HEAD']) : null;
627
+ const commitOrNull = (value) => (value && !/^0+$/.test(value) ? value : null);
628
+
629
+ return {
630
+ headCommit: commitOrNull(head),
631
+ indexCommit: commitOrNull(index),
632
+ worktreeCommit: worktree?.success ? worktree.stdout.trim() : null,
633
+ hasTrackedChanges: flags[2] === 'M',
634
+ hasUntrackedFiles: flags[3] === 'U',
635
+ hasConflict,
636
+ };
543
637
  };
544
638
 
545
639
  const cleanBranchName = (branch) => {
@@ -925,13 +1019,16 @@ const isMissingDirectoryError = (error) => {
925
1019
  return /directory that does not exist|does not exist|no such file or directory/i.test(text);
926
1020
  };
927
1021
 
928
- const runGitCommand = async (cwd, args) => {
1022
+ const runGitCommand = async (cwd, args, { timeoutMs = 0 } = {}) => {
929
1023
  try {
930
1024
  const { stdout, stderr } = await execFileAsync(getGitBinary(), args, {
931
1025
  cwd,
932
1026
  env: await buildGitEnv(),
933
1027
  windowsHide: true,
934
1028
  maxBuffer: 20 * 1024 * 1024,
1029
+ // Only short probes pass a timeout; commands that legitimately run long
1030
+ // (a fetch into a temporary clone) keep the default of none.
1031
+ ...(timeoutMs > 0 ? { timeout: timeoutMs, killSignal: 'SIGKILL' } : {}),
935
1032
  });
936
1033
  return {
937
1034
  success: true,
@@ -2058,14 +2155,45 @@ const applyUpstreamConfiguration = async (args) => {
2058
2155
  );
2059
2156
  };
2060
2157
 
2158
+ /**
2159
+ * A repository whose root is the user's home directory or a filesystem root
2160
+ * (`C:\`, `/`) covers the whole disk. Every status read walks Program Files
2161
+ * or the entire home tree, which is minutes of Git work per refresh and, on
2162
+ * Windows, the process pile-ups users report. Such a repository is nearly
2163
+ * always an accidental `git init` in the wrong place, so OpenChamber treats
2164
+ * it as no repository at all. Returns the reason or null for a normal root.
2165
+ */
2166
+ export const unsupportedRepositoryRootReason = (repoRoot, home = os.homedir()) => {
2167
+ if (typeof repoRoot !== 'string' || !repoRoot.trim()) return null;
2168
+ const resolved = path.resolve(repoRoot.trim());
2169
+ if (path.resolve(path.parse(resolved).root) === resolved) return 'filesystem-root';
2170
+ if (typeof home === 'string' && home.trim() && path.resolve(home.trim()) === resolved) return 'home';
2171
+ return null;
2172
+ };
2173
+
2174
+ const warnedUnsupportedRoots = new Set();
2175
+
2061
2176
  export async function isGitRepository(directory) {
2062
2177
  const directoryPath = normalizeDirectoryPath(directory);
2063
2178
  if (!directoryPath || !fs.existsSync(directoryPath)) {
2064
2179
  return false;
2065
2180
  }
2066
2181
 
2067
- const result = await runGitCommand(directoryPath, ['rev-parse', '--git-dir']);
2068
- return result.success;
2182
+ const result = await runGitCommand(directoryPath, ['rev-parse', '--git-dir'], { timeoutMs: GIT_PROBE_TIMEOUT_MS });
2183
+ if (!result.success) return false;
2184
+
2185
+ // `--show-toplevel` has no answer inside a bare repository or a .git
2186
+ // directory; those keep the previous answer rather than being rejected.
2187
+ const topLevel = await runGitCommand(directoryPath, ['rev-parse', '--show-toplevel'], { timeoutMs: GIT_PROBE_TIMEOUT_MS });
2188
+ if (!topLevel.success) return true;
2189
+ const repoRoot = topLevel.stdout.trim();
2190
+ const reason = unsupportedRepositoryRootReason(repoRoot);
2191
+ if (!reason) return true;
2192
+ if (!warnedUnsupportedRoots.has(repoRoot)) {
2193
+ warnedUnsupportedRoots.add(repoRoot);
2194
+ console.warn(`[git] Ignoring repository rooted at ${repoRoot} (${reason}): Git features are disabled for ${directoryPath}`);
2195
+ }
2196
+ return false;
2069
2197
  }
2070
2198
 
2071
2199
  export async function getGlobalIdentity() {
@@ -2187,13 +2315,193 @@ export async function setLocalIdentity(directory, profile) {
2187
2315
  }
2188
2316
  }
2189
2317
 
2318
+ // Beyond this many untracked files, a directory stays one `dir/` entry in
2319
+ // status. Every file would otherwise become a row, a diff request, and a stat
2320
+ // on the server, and the only directories that large are ones that belong in
2321
+ // .gitignore.
2322
+ const UNTRACKED_DIRECTORY_EXPANSION_LIMIT = 1000;
2323
+
2324
+ // A status read holds one of MAX_CONCURRENT_STATUS_READS slots until it
2325
+ // finishes. Git never gets a terminal here, but a process can still hang on
2326
+ // Windows (a locked index, a stuck filesystem monitor, an unreachable network
2327
+ // drive), and a hung process would hold its slot forever: four of them and no
2328
+ // status read runs again until someone kills them by hand. Every process the
2329
+ // read spawns is therefore killed when it stops producing output for this long,
2330
+ // and the read fails instead of wedging the limiter. Two minutes is far above
2331
+ // what a healthy read spends silent, even on a very large tree.
2332
+ const GIT_STATUS_STALL_TIMEOUT_MS = 120_000;
2333
+ const GIT_UNTRACKED_LISTING_STALL_TIMEOUT_MS = 60_000;
2334
+ const GIT_PROBE_TIMEOUT_MS = 30_000;
2335
+
2336
+ // Untracked files under `dirPath` (repository-relative, trailing slash), read
2337
+ // Git for Windows runs commands through a launcher: the `git.exe` we spawn is a
2338
+ // wrapper whose child is the real `git`. Killing only the wrapper leaves that
2339
+ // child alive, still walking the tree on its own (a repository rooted at a
2340
+ // drive root sends it through Program Files), and it shows up in Task Manager
2341
+ // as a stuck pair until someone ends it by hand. Windows has no process groups
2342
+ // to signal, so the tree is ended through taskkill.
2343
+ const killProcessTree = (child) => {
2344
+ if (!child.pid) return;
2345
+ if (process.platform === 'win32') {
2346
+ try {
2347
+ spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' }).on('error', () => {});
2348
+ } catch {
2349
+ child.kill('SIGKILL');
2350
+ }
2351
+ return;
2352
+ }
2353
+ child.kill('SIGKILL');
2354
+ };
2355
+
2356
+ // from a streamed `ls-files` that is stopped once the bound is exceeded so a
2357
+ // huge directory is never listed in full. `paths` is complete when
2358
+ // `truncated` is false.
2359
+ const listUntrackedFilesBounded = async (repoRoot, dirPath, limit) => {
2360
+ const env = await buildGitEnv();
2361
+ return new Promise((resolve, reject) => {
2362
+ const child = spawn(getGitBinary(), ['ls-files', '--others', '--exclude-standard', '-z', '--', dirPath], {
2363
+ cwd: repoRoot,
2364
+ env,
2365
+ windowsHide: true,
2366
+ stdio: ['ignore', 'pipe', 'ignore'],
2367
+ });
2368
+ const paths = [];
2369
+ let pending = '';
2370
+ let truncated = false;
2371
+ let settled = false;
2372
+ let stallTimer = null;
2373
+ const finish = (error) => {
2374
+ if (settled) return;
2375
+ settled = true;
2376
+ if (stallTimer) clearTimeout(stallTimer);
2377
+ if (error) {
2378
+ reject(error);
2379
+ return;
2380
+ }
2381
+ resolve({ paths, truncated });
2382
+ };
2383
+ // A listing that goes silent is killed rather than left holding the
2384
+ // status read (and its limiter slot) open.
2385
+ const armStallTimer = () => {
2386
+ if (stallTimer) clearTimeout(stallTimer);
2387
+ stallTimer = setTimeout(() => {
2388
+ killProcessTree(child);
2389
+ finish(new Error(`git ls-files produced no output for ${GIT_UNTRACKED_LISTING_STALL_TIMEOUT_MS}ms in ${dirPath}`));
2390
+ }, GIT_UNTRACKED_LISTING_STALL_TIMEOUT_MS);
2391
+ };
2392
+ armStallTimer();
2393
+ child.stdout.on('data', (chunk) => {
2394
+ armStallTimer();
2395
+ if (truncated) return;
2396
+ pending += chunk.toString('utf8');
2397
+ const records = pending.split('\0');
2398
+ pending = records.pop() ?? '';
2399
+ for (const record of records) {
2400
+ if (!record) continue;
2401
+ paths.push(record);
2402
+ if (paths.length > limit) {
2403
+ truncated = true;
2404
+ killProcessTree(child);
2405
+ finish();
2406
+ return;
2407
+ }
2408
+ }
2409
+ });
2410
+ child.on('error', (error) => finish(error));
2411
+ child.on('close', (code) => {
2412
+ if (truncated) {
2413
+ finish();
2414
+ return;
2415
+ }
2416
+ if (code !== 0) {
2417
+ finish(new Error(`git ls-files exited with code ${code} for ${dirPath}`));
2418
+ return;
2419
+ }
2420
+ if (pending) paths.push(pending);
2421
+ finish();
2422
+ });
2423
+ });
2424
+ };
2425
+
2426
+ // Replaces each untracked `dir/` entry from `-unormal` with one entry per file
2427
+ // inside it, the listing `-uall` would have produced, unless the directory
2428
+ // holds more than the bound; then the `dir/` entry stays. A nested repository
2429
+ // lists as itself and stays a `dir/` entry too, which is what the diff routes
2430
+ // expect. A listing failure keeps the `dir/` entry rather than dropping the
2431
+ // change from the status.
2432
+ const expandUntrackedDirectories = async (repoRoot, files) => {
2433
+ const expanded = [];
2434
+ for (const file of files) {
2435
+ const isUntrackedDirectory = file.path.endsWith('/')
2436
+ && (file.working_dir || '').trim() === '?'
2437
+ && (file.index || '').trim() === '?';
2438
+ if (!isUntrackedDirectory) {
2439
+ expanded.push(file);
2440
+ continue;
2441
+ }
2442
+ const listing = await listUntrackedFilesBounded(repoRoot, file.path, UNTRACKED_DIRECTORY_EXPANSION_LIMIT)
2443
+ .catch((error) => {
2444
+ console.warn(`[GitService] Could not expand untracked directory ${file.path}:`, error?.message || error);
2445
+ return null;
2446
+ });
2447
+ if (!listing || listing.truncated || listing.paths.some((entry) => entry === file.path)) {
2448
+ expanded.push(file);
2449
+ continue;
2450
+ }
2451
+ for (const entryPath of listing.paths) {
2452
+ expanded.push({ ...file, path: entryPath });
2453
+ }
2454
+ }
2455
+ return expanded;
2456
+ };
2457
+
2458
+ // A status read walks the working tree and runs a dozen Git processes; on a
2459
+ // large repository it takes seconds. Clients ask for it after every completed
2460
+ // agent tool call, from several surfaces, and from PR polling, so without a
2461
+ // bound one slow repository ends up with many identical `git status` processes
2462
+ // side by side. Runs are serialized per directory and capped across
2463
+ // directories; a caller that asks during a run gets a run started after it
2464
+ // asked, so results are never older than the request.
2465
+ const MAX_CONCURRENT_STATUS_READS = 4;
2466
+ const statusRefresh = createSerialRefresh({ maxConcurrent: MAX_CONCURRENT_STATUS_READS });
2467
+
2190
2468
  export async function getStatus(directory, options = {}) {
2191
- const lightMode = options.mode === 'light';
2192
2469
  const normalizedDirectory = normalizeDirectoryPath(directory);
2193
2470
  if (typeof normalizedDirectory !== 'string' || !normalizedDirectory.trim()) {
2194
2471
  throw new Error('directory is required');
2195
2472
  }
2473
+ const lightMode = options.mode === 'light';
2474
+ // A full read satisfies light callers too, so one run serves whichever
2475
+ // callers it answers, at the widest mode any of them asked for.
2476
+ return statusRefresh.run(
2477
+ normalizedDirectory,
2478
+ { lightMode },
2479
+ (requests) => readStatus(normalizedDirectory, requests.every((request) => request.lightMode)),
2480
+ );
2481
+ }
2482
+
2483
+ /**
2484
+ * Upstream of the checked-out branch as `remote/branch`, or `null` when HEAD
2485
+ * is detached, unborn, or the branch has no upstream configured. Reads refs
2486
+ * and config only, never the working tree: callers that only need the
2487
+ * tracking name must not pay for a status read.
2488
+ */
2489
+ export async function getTrackingBranch(directory) {
2490
+ const normalizedDirectory = normalizeDirectoryPath(directory);
2491
+ if (!normalizedDirectory) {
2492
+ return null;
2493
+ }
2494
+ const head = await runGitCommand(normalizedDirectory, ['symbolic-ref', '--quiet', 'HEAD']);
2495
+ const headRef = head.success ? head.stdout.trim() : '';
2496
+ if (!headRef.startsWith('refs/heads/')) {
2497
+ return null;
2498
+ }
2499
+ const upstream = await runGitCommand(normalizedDirectory, ['for-each-ref', '--format=%(upstream:short)', headRef]);
2500
+ const tracking = upstream.success ? upstream.stdout.trim() : '';
2501
+ return tracking || null;
2502
+ }
2196
2503
 
2504
+ async function readStatus(normalizedDirectory, lightMode) {
2197
2505
  try {
2198
2506
  // Prefer an explicit non-repo check before simple-git status so a missing
2199
2507
  // repository never depends on process.cwd() or an opaque GitError shape.
@@ -2201,12 +2509,22 @@ export async function getStatus(directory, options = {}) {
2201
2509
  throw new Error('fatal: not a git repository (or any of the parent directories): .git');
2202
2510
  }
2203
2511
 
2204
- const { directoryPath, repoRoot, git } = await createRepositoryGitContext(normalizedDirectory);
2205
-
2206
- // Use -uall to show all untracked files individually, not just directories
2207
- const status = await git.status(['-uall']);
2512
+ const { directoryPath, repoRoot, git } = await createRepositoryGitContext(normalizedDirectory, {
2513
+ stallTimeoutMs: GIT_STATUS_STALL_TIMEOUT_MS,
2514
+ });
2208
2515
 
2209
- // Light mode: skip numstat + new-file line counting for faster response
2516
+ // `-unormal` lists a directory with no tracked files as one `dir/` entry
2517
+ // and stops walking it at its first file. `-uall` would walk every file
2518
+ // in it: on a forgotten build or dependency directory that is a scan of
2519
+ // tens of thousands of files and hundreds of megabytes per status read.
2520
+ // Directories are expanded to their files afterwards, up to a bound.
2521
+ const status = await git.status(['-unormal']);
2522
+ status.files = await expandUntrackedDirectories(repoRoot, status.files);
2523
+
2524
+ // Light mode: skip numstat + new-file line counting for faster response.
2525
+ // Staged (`--cached`: HEAD -> index) and working (`--numstat`: index -> worktree)
2526
+ // stay in separate maps. A partially staged file has an entry in both, and the
2527
+ // UI shows each row's own scope instead of a combined total.
2210
2528
  const [stagedStatsRaw, workingStatsRaw] = lightMode
2211
2529
  ? ['', '']
2212
2530
  : await Promise.all([
@@ -2214,9 +2532,10 @@ export async function getStatus(directory, options = {}) {
2214
2532
  git.raw(['diff', '--numstat']).catch(() => ''),
2215
2533
  ]);
2216
2534
 
2217
- const diffStatsMap = new Map();
2535
+ const stagedDiffStats = {};
2536
+ const workingDiffStats = {};
2218
2537
 
2219
- const accumulateStats = (raw) => {
2538
+ const accumulateStats = (raw, target) => {
2220
2539
  if (!raw) return;
2221
2540
  raw
2222
2541
  .split('\n')
@@ -2235,18 +2554,18 @@ export async function getStatus(directory, options = {}) {
2235
2554
  const insertions = insertionsRaw === '-' ? 0 : parseInt(insertionsRaw, 10) || 0;
2236
2555
  const deletions = deletionsRaw === '-' ? 0 : parseInt(deletionsRaw, 10) || 0;
2237
2556
 
2238
- const existing = diffStatsMap.get(path) || { insertions: 0, deletions: 0 };
2239
- diffStatsMap.set(path, {
2557
+ const existing = target[path] || { insertions: 0, deletions: 0 };
2558
+ target[path] = {
2240
2559
  insertions: existing.insertions + insertions,
2241
2560
  deletions: existing.deletions + deletions,
2242
- });
2561
+ };
2243
2562
  });
2244
2563
  };
2245
2564
 
2246
- accumulateStats(stagedStatsRaw);
2247
- accumulateStats(workingStatsRaw);
2565
+ accumulateStats(stagedStatsRaw, stagedDiffStats);
2566
+ accumulateStats(workingStatsRaw, workingDiffStats);
2248
2567
 
2249
- const diffStats = Object.fromEntries(diffStatsMap.entries());
2568
+ const diffStats = { staged: stagedDiffStats, working: workingDiffStats };
2250
2569
 
2251
2570
  const MAX_NEW_FILE_STATS = 200;
2252
2571
  const MAX_NEW_FILE_STAT_SIZE = 1024 * 1024;
@@ -2266,7 +2585,10 @@ export async function getStatus(directory, options = {}) {
2266
2585
  continue;
2267
2586
  }
2268
2587
 
2269
- const existing = diffStats[file.path];
2588
+ // Untracked and working-tree-added files belong to the working scope;
2589
+ // a file whose 'A' code is on the index belongs to the staged scope.
2590
+ const target = working === '?' || working === 'A' ? workingDiffStats : stagedDiffStats;
2591
+ const existing = target[file.path];
2270
2592
  if (existing && existing.insertions > 0) {
2271
2593
  continue;
2272
2594
  }
@@ -2282,6 +2604,7 @@ export async function getStatus(directory, options = {}) {
2282
2604
  const buffer = await fsp.readFile(absolutePath);
2283
2605
  if (buffer.indexOf(0) !== -1) {
2284
2606
  newFileStats.push({
2607
+ target,
2285
2608
  path: file.path,
2286
2609
  insertions: existing?.insertions ?? 0,
2287
2610
  deletions: existing?.deletions ?? 0,
@@ -2292,6 +2615,7 @@ export async function getStatus(directory, options = {}) {
2292
2615
  const normalized = buffer.toString('utf8').replace(/\r\n/g, '\n');
2293
2616
  if (!normalized.length) {
2294
2617
  newFileStats.push({
2618
+ target,
2295
2619
  path: file.path,
2296
2620
  insertions: 0,
2297
2621
  deletions: 0,
@@ -2306,6 +2630,7 @@ export async function getStatus(directory, options = {}) {
2306
2630
 
2307
2631
  const lineCount = segments.length;
2308
2632
  newFileStats.push({
2633
+ target,
2309
2634
  path: file.path,
2310
2635
  insertions: lineCount,
2311
2636
  deletions: 0,
@@ -2319,7 +2644,7 @@ export async function getStatus(directory, options = {}) {
2319
2644
  }
2320
2645
 
2321
2646
  for (const entry of newFileStats) {
2322
- diffStats[entry.path] = {
2647
+ entry.target[entry.path] = {
2323
2648
  insertions: entry.insertions,
2324
2649
  deletions: entry.deletions,
2325
2650
  };
@@ -2481,11 +2806,29 @@ const getNoIndexDiff = async (repoRoot, repoPath, contextLines) => {
2481
2806
  };
2482
2807
 
2483
2808
  export async function getDiff(directory, { path: filePath, staged = false, contextLines = 3 } = {}) {
2484
- const { directoryPath, directoryGit, repoRoot, git } = await createRepositoryGitContext(directory);
2809
+ const context = await createRepositoryGitContext(directory);
2810
+ const fileContext = filePath
2811
+ ? await resolveGitFileContext(context.directoryPath, context.directoryGit, filePath, context.repoRoot)
2812
+ : null;
2813
+ return readDiff(context, fileContext, { staged, contextLines });
2814
+ }
2815
+
2816
+ /**
2817
+ * `getDiff` for one path, plus what a submodule records. A submodule patch is
2818
+ * empty when only untracked files changed inside it, so callers need the state
2819
+ * to show anything truthful.
2820
+ */
2821
+ export async function getPathDiff(directory, { path: filePath, staged = false, contextLines = 3 } = {}) {
2822
+ const context = await createRepositoryGitContext(directory);
2823
+ const fileContext = await resolveGitFileContext(context.directoryPath, context.directoryGit, filePath, context.repoRoot);
2824
+ const diff = await readDiff(context, fileContext, { staged, contextLines });
2825
+ if (!fileContext.isSubmodule) return { diff, submodule: null };
2826
+ return { diff, submodule: await readSubmoduleState(context.repoRoot, fileContext) };
2827
+ }
2485
2828
 
2829
+ async function readDiff({ repoRoot, git }, fileContext, { staged, contextLines }) {
2486
2830
  try {
2487
2831
  const args = ['diff', '--no-color', '--full-index'];
2488
- const fileContext = filePath ? await resolveGitFileContext(directoryPath, directoryGit, filePath, repoRoot) : null;
2489
2832
 
2490
2833
  if (typeof contextLines === 'number' && !Number.isNaN(contextLines)) {
2491
2834
  args.push(`-U${Math.max(0, contextLines)}`);
@@ -2682,7 +3025,7 @@ export async function getRangeDiff(directory, { base, head, path: filePath, cont
2682
3025
  const fileContext = await resolveGitFileContext(directoryPath, directoryGit, filePath, repoRoot);
2683
3026
  paths.push(fileContext.repoPath);
2684
3027
  } catch (error) {
2685
- if (error.message !== 'Invalid file path') throw error;
3028
+ if (error.code !== GIT_PATH_NOT_FOUND) throw error;
2686
3029
  // A committed deletion is absent from HEAD, the index, and the working
2687
3030
  // tree. It is still a valid range path when it exists at the merge base.
2688
3031
  const mergeBase = (await git.raw(['merge-base', baseRef, headRef])).trim();
@@ -2907,7 +3250,22 @@ export async function getFileDiff(directory, { path: filePath, staged = false }
2907
3250
  const { directoryPath, directoryGit, repoRoot, git } = await createRepositoryGitContext(directory);
2908
3251
  const isImage = isImageFile(filePath);
2909
3252
  const mimeType = isImage ? getImageMimeType(filePath) : null;
2910
- const { absolutePath, repoPath, isSymbolicLink } = await resolveGitFileContext(directoryPath, directoryGit, filePath, repoRoot);
3253
+ const fileContext = await resolveGitFileContext(directoryPath, directoryGit, filePath, repoRoot);
3254
+ const { absolutePath, repoPath, isSymbolicLink } = fileContext;
3255
+
3256
+ if (fileContext.isSubmodule) {
3257
+ // Git's own text form of a gitlink, so a plain two-pane view still shows
3258
+ // the recorded commits; `submodule` carries what the text cannot.
3259
+ const submodule = await readSubmoduleState(repoRoot, fileContext);
3260
+ const describeCommit = (commit) => (commit ? `Subproject commit ${commit}\n` : '');
3261
+ return {
3262
+ original: describeCommit(submodule.headCommit),
3263
+ modified: describeCommit(staged ? submodule.indexCommit : submodule.worktreeCommit),
3264
+ path: filePath,
3265
+ isBinary: false,
3266
+ submodule,
3267
+ };
3268
+ }
2911
3269
 
2912
3270
  if (!isImage && !isSymbolicLink) {
2913
3271
  const isBinaryBySniff = await looksBinaryBySniff(absolutePath);
@@ -3346,26 +3704,45 @@ export async function push(directory, options = {}) {
3346
3704
  );
3347
3705
  };
3348
3706
 
3349
- const normalizePushResult = (result) => {
3707
+ const remote = String(options.remote || '').trim();
3708
+ const status = await git.status();
3709
+ const config = await git.listConfig();
3710
+ const remotes = await git.getRemotes(true);
3711
+ const remoteName = remote
3712
+ || config.all[`branch.${status.current}.pushremote`]
3713
+ || config.all['remote.pushdefault']
3714
+ || config.all[`branch.${status.current}.remote`]
3715
+ || (remotes.length === 1 ? remotes[0].name : 'origin');
3716
+
3717
+ const pushTo = async (target, branch, pushOptions) => {
3718
+ // simple-git drops forced updates and puts no-ops in `pushed`. Read Git's
3719
+ // porcelain status flags so feedback reflects actual remote ref changes.
3720
+ let output = '';
3721
+ git.outputHandler((_command, stdout) => {
3722
+ stdout.on('data', (chunk) => { output += chunk.toString(); });
3723
+ });
3724
+ const result = await git.push(target, branch, pushOptions);
3725
+ const pushed = [];
3726
+ for (const line of output.split(/\r?\n/)) {
3727
+ const match = /^([ *+\-])\t([^:]*):([^\t]+)\t/.exec(line);
3728
+ if (match) {
3729
+ pushed.push({ local: match[2], remote: remoteName });
3730
+ }
3731
+ }
3350
3732
  return {
3351
3733
  success: true,
3352
- pushed: result.pushed,
3353
- repo: result.repo,
3354
- ref: result.ref,
3734
+ pushed,
3735
+ repo: result.repo || directory,
3736
+ ref: result.ref || null,
3355
3737
  };
3356
3738
  };
3357
3739
 
3358
- const remote = String(options.remote || '').trim();
3359
-
3360
3740
  if (!remote && !options.branch) {
3361
3741
  try {
3362
- await git.push();
3363
- return {
3364
- success: true,
3365
- pushed: [],
3366
- repo: directory,
3367
- ref: null,
3368
- };
3742
+ const pushOptions = status.current && !status.tracking
3743
+ ? buildUpstreamOptions(options.options)
3744
+ : options.options || {};
3745
+ return await pushTo(undefined, undefined, pushOptions);
3369
3746
  } catch (error) {
3370
3747
  if (!looksLikeMissingUpstream(error)) {
3371
3748
  const message = describePushError(error);
@@ -3374,17 +3751,13 @@ export async function push(directory, options = {}) {
3374
3751
  }
3375
3752
 
3376
3753
  try {
3377
- const status = await git.status();
3378
3754
  const branch = status.current;
3379
- const remotes = await git.getRemotes(true);
3380
- const fallbackRemote = remotes.find((entry) => entry.name === 'origin')?.name || remotes[0]?.name;
3381
- if (!branch || !fallbackRemote) {
3755
+ if (!branch || !remoteName) {
3382
3756
  const message = describePushError(error);
3383
3757
  throw new Error(message);
3384
3758
  }
3385
3759
 
3386
- const result = await git.push(fallbackRemote, branch, buildUpstreamOptions(options.options));
3387
- return normalizePushResult(result);
3760
+ return await pushTo(remoteName, branch, buildUpstreamOptions(options.options));
3388
3761
  } catch (fallbackError) {
3389
3762
  const message = describePushError(fallbackError);
3390
3763
  console.error('Failed to push (including upstream fallback):', fallbackError);
@@ -3393,26 +3766,14 @@ export async function push(directory, options = {}) {
3393
3766
  }
3394
3767
  }
3395
3768
 
3396
- const remoteName = remote || 'origin';
3397
-
3398
3769
  // If caller didn't specify a branch, this is the common "Push"/"Commit & Push" path.
3399
3770
  // When there's no upstream yet (typical for freshly-created worktree branches), publish it on first push.
3400
- if (!options.branch) {
3401
- try {
3402
- const status = await git.status();
3403
- if (status.current && !status.tracking) {
3404
- const result = await git.push(remoteName, status.current, buildUpstreamOptions(options.options));
3405
- return normalizePushResult(result);
3406
- }
3407
- } catch (error) {
3408
- // If we can't read status, fall back to the regular push path below.
3409
- console.warn('Failed to read git status before push:', error);
3410
- }
3771
+ if (!options.branch && status.current && !status.tracking) {
3772
+ return pushTo(remoteName, status.current, buildUpstreamOptions(options.options));
3411
3773
  }
3412
3774
 
3413
3775
  try {
3414
- const result = await git.push(remoteName, options.branch, options.options || {});
3415
- return normalizePushResult(result);
3776
+ return await pushTo(remoteName, options.branch, options.options || {});
3416
3777
  } catch (error) {
3417
3778
  // Last-resort fallback: retry with upstream if the error suggests it's missing.
3418
3779
  if (!looksLikeMissingUpstream(error)) {
@@ -3422,15 +3783,13 @@ export async function push(directory, options = {}) {
3422
3783
  }
3423
3784
 
3424
3785
  try {
3425
- const status = await git.status();
3426
3786
  const branch = options.branch || status.current;
3427
3787
  if (!branch) {
3428
3788
  console.error('Failed to push: missing branch name for upstream setup:', error);
3429
3789
  throw error;
3430
3790
  }
3431
3791
 
3432
- const result = await git.push(remoteName, branch, buildUpstreamOptions(options.options));
3433
- return normalizePushResult(result);
3792
+ return await pushTo(remoteName, branch, buildUpstreamOptions(options.options));
3434
3793
  } catch (fallbackError) {
3435
3794
  const message = describePushError(fallbackError);
3436
3795
  console.error('Failed to push (including upstream fallback):', fallbackError);
@@ -5057,7 +5416,7 @@ export async function canonicalizeWorktreeState(directory) {
5057
5416
 
5058
5417
  // Detect attention reasons from getStatus side-effects
5059
5418
  try {
5060
- const status = await git.status(['-uall']);
5419
+ const status = await git.status(['-unormal']);
5061
5420
  if (status.current && (await git.raw(['rev-parse', '--verify', 'MERGE_HEAD']).then(() => true).catch(() => false))) {
5062
5421
  attentionReason = 'merge';
5063
5422
  } else {