@rexvane/piweb 0.3.25 → 0.3.26

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 (345) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +6 -5
  3. package/.next/build-manifest.json +4 -4
  4. package/.next/next-server.js.nft.json +1 -1
  5. package/.next/prerender-manifest.json +4 -4
  6. package/.next/react-loadable-manifest.json +14 -26
  7. package/.next/required-server-files.js +26 -26
  8. package/.next/required-server-files.json +26 -26
  9. package/.next/routes-manifest.json +6 -0
  10. package/.next/server/app/_global-error/page.js +2 -2
  11. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  12. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  13. package/.next/server/app/_global-error.html +1 -1
  14. package/.next/server/app/_global-error.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  16. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  17. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  18. package/.next/server/app/_not-found/page.js +1 -1
  19. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  20. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  21. package/.next/server/app/_not-found.html +1 -1
  22. package/.next/server/app/_not-found.rsc +2 -2
  23. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  24. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  25. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  26. package/.next/server/app/api/agent/[id]/events/route.js +2 -2
  27. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
  28. package/.next/server/app/api/agent/[id]/events/route_client-reference-manifest.js +1 -1
  29. package/.next/server/app/api/agent/[id]/route.js +1 -1
  30. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
  31. package/.next/server/app/api/agent/[id]/route_client-reference-manifest.js +1 -1
  32. package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
  33. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
  34. package/.next/server/app/api/agent/[id]/tree/route_client-reference-manifest.js +1 -1
  35. package/.next/server/app/api/agent/new/route.js +1 -1
  36. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  37. package/.next/server/app/api/agent/new/route_client-reference-manifest.js +1 -1
  38. package/.next/server/app/api/agent/resources/route.js +1 -0
  39. package/.next/server/app/api/agent/resources/route.js.nft.json +1 -0
  40. package/.next/server/app/api/agent/resources/route_client-reference-manifest.js +1 -0
  41. package/.next/server/app/api/config/route.js +1 -1
  42. package/.next/server/app/api/config/route.js.nft.json +1 -1
  43. package/.next/server/app/api/config/route_client-reference-manifest.js +1 -1
  44. package/.next/server/app/api/files/route.js +1 -1
  45. package/.next/server/app/api/files/route.js.nft.json +1 -1
  46. package/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  47. package/.next/server/app/api/fs/route.js +1 -1
  48. package/.next/server/app/api/fs/route.js.nft.json +1 -1
  49. package/.next/server/app/api/fs/route_client-reference-manifest.js +1 -1
  50. package/.next/server/app/api/git/route.js +4 -4
  51. package/.next/server/app/api/git/route.js.nft.json +1 -1
  52. package/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  53. package/.next/server/app/api/github/route.js +4 -4
  54. package/.next/server/app/api/github/route.js.nft.json +1 -1
  55. package/.next/server/app/api/github/route_client-reference-manifest.js +1 -1
  56. package/.next/server/app/api/growth/route.js +1 -1
  57. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  58. package/.next/server/app/api/growth/route_client-reference-manifest.js +1 -1
  59. package/.next/server/app/api/health/route.js +1 -1
  60. package/.next/server/app/api/health/route.js.nft.json +1 -1
  61. package/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
  62. package/.next/server/app/api/models/defaults/route.js +1 -1
  63. package/.next/server/app/api/models/defaults/route.js.nft.json +1 -1
  64. package/.next/server/app/api/models/defaults/route_client-reference-manifest.js +1 -1
  65. package/.next/server/app/api/models/route.js +1 -1
  66. package/.next/server/app/api/models/route.js.nft.json +1 -1
  67. package/.next/server/app/api/models/route_client-reference-manifest.js +1 -1
  68. package/.next/server/app/api/pi-settings/route.js +1 -1
  69. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  70. package/.next/server/app/api/pi-settings/route_client-reference-manifest.js +1 -1
  71. package/.next/server/app/api/plugins/route.js +1 -1
  72. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  73. package/.next/server/app/api/plugins/route_client-reference-manifest.js +1 -1
  74. package/.next/server/app/api/prompts/route.js +1 -1
  75. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  76. package/.next/server/app/api/prompts/route_client-reference-manifest.js +1 -1
  77. package/.next/server/app/api/remote/route.js +2 -2
  78. package/.next/server/app/api/remote/route.js.nft.json +1 -1
  79. package/.next/server/app/api/remote/route_client-reference-manifest.js +1 -1
  80. package/.next/server/app/api/security/route.js +1 -1
  81. package/.next/server/app/api/security/route.js.nft.json +1 -1
  82. package/.next/server/app/api/security/route_client-reference-manifest.js +1 -1
  83. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  84. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  85. package/.next/server/app/api/sessions/[id]/export/route_client-reference-manifest.js +1 -1
  86. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  87. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  88. package/.next/server/app/api/sessions/[id]/route_client-reference-manifest.js +1 -1
  89. package/.next/server/app/api/sessions/import/route.js +2 -2
  90. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
  91. package/.next/server/app/api/sessions/import/route_client-reference-manifest.js +1 -1
  92. package/.next/server/app/api/sessions/route.js +1 -1
  93. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  94. package/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
  95. package/.next/server/app/api/skills/route.js +2 -2
  96. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  97. package/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
  98. package/.next/server/app/api/update/route.js +2 -2
  99. package/.next/server/app/api/update/route.js.nft.json +1 -1
  100. package/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  101. package/.next/server/app/api/version/route.js +1 -1
  102. package/.next/server/app/api/version/route.js.nft.json +1 -1
  103. package/.next/server/app/api/version/route_client-reference-manifest.js +1 -1
  104. package/.next/server/app/api/web-auth/route.js +2 -2
  105. package/.next/server/app/api/web-auth/route.js.nft.json +1 -1
  106. package/.next/server/app/api/web-auth/route_client-reference-manifest.js +1 -1
  107. package/.next/server/app/api/workspaces/route.js +1 -1
  108. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  109. package/.next/server/app/api/workspaces/route_client-reference-manifest.js +1 -1
  110. package/.next/server/app/icon.svg/route.js +1 -1
  111. package/.next/server/app/icon.svg/route.js.nft.json +1 -1
  112. package/.next/server/app/index.html +1 -1
  113. package/.next/server/app/index.rsc +5 -5
  114. package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
  115. package/.next/server/app/index.segments/_full.segment.rsc +5 -5
  116. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  117. package/.next/server/app/login/page.js +1 -1
  118. package/.next/server/app/login/page.js.nft.json +1 -1
  119. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  120. package/.next/server/app/login.html +1 -1
  121. package/.next/server/app/login.rsc +4 -4
  122. package/.next/server/app/login.segments/_full.segment.rsc +4 -4
  123. package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
  124. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +4 -4
  125. package/.next/server/app/page.js +1 -1
  126. package/.next/server/app/page.js.nft.json +1 -1
  127. package/.next/server/app/page_client-reference-manifest.js +1 -1
  128. package/.next/server/app-paths-manifest.json +6 -5
  129. package/.next/server/chunks/{813.js → 1813.js} +1 -1
  130. package/.next/server/chunks/{430.js → 2430.js} +5 -5
  131. package/.next/server/chunks/{970.js → 2970.js} +1 -1
  132. package/.next/server/chunks/3924.js +6 -0
  133. package/.next/server/chunks/4807.js +1 -0
  134. package/.next/server/chunks/5393.js +1 -0
  135. package/.next/server/chunks/5802.js +2 -0
  136. package/.next/server/chunks/620.js +1 -1
  137. package/.next/server/chunks/6229.js +1 -0
  138. package/.next/server/chunks/7496.js +1 -0
  139. package/.next/server/chunks/7662.js +2 -0
  140. package/.next/server/chunks/8686.js +1 -0
  141. package/.next/server/chunks/{593.js → 974.js} +1 -1
  142. package/.next/server/chunks/{920.js → 9920.js} +1 -1
  143. package/.next/server/instrumentation.js +1 -1
  144. package/.next/server/instrumentation.js.nft.json +1 -1
  145. package/.next/server/middleware-build-manifest.js +1 -1
  146. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  147. package/.next/server/middleware.js +5 -5
  148. package/.next/server/pages/404.html +1 -1
  149. package/.next/server/pages/500.html +1 -1
  150. package/.next/server/preview-props.json +3 -3
  151. package/.next/server/server-reference-manifest.json +1 -1
  152. package/.next/server/webpack-runtime.js +1 -1
  153. package/.next/static/QJCtqgDKzGS2BdKABd9ch/_buildManifest.js +1 -0
  154. package/.next/static/chunks/2577.86c8538fbf05a8b2.js +1 -0
  155. package/.next/static/chunks/2716.fa561519675a3ff1.js +1 -0
  156. package/.next/static/chunks/2906.8a0e09bb47605cf1.js +1 -0
  157. package/.next/static/chunks/4673.ea36e55005c0f82f.js +1 -0
  158. package/.next/static/chunks/4686-4dc565817a160351.js +1 -0
  159. package/.next/static/chunks/4778.0d2c5d75b791d352.js +1 -0
  160. package/.next/static/chunks/app/_global-error/page-f2d93969c0bcf406.js +1 -0
  161. package/.next/static/chunks/app/_not-found/page-2d40d1d7d916666a.js +1 -0
  162. package/.next/static/chunks/app/api/agent/[id]/events/route-f2d93969c0bcf406.js +1 -0
  163. package/.next/static/chunks/app/api/agent/[id]/route-f2d93969c0bcf406.js +1 -0
  164. package/.next/static/chunks/app/api/agent/[id]/tree/route-f2d93969c0bcf406.js +1 -0
  165. package/.next/static/chunks/app/api/agent/new/route-f2d93969c0bcf406.js +1 -0
  166. package/.next/static/chunks/app/api/agent/resources/route-f2d93969c0bcf406.js +1 -0
  167. package/.next/static/chunks/app/api/config/route-f2d93969c0bcf406.js +1 -0
  168. package/.next/static/chunks/app/api/files/route-f2d93969c0bcf406.js +1 -0
  169. package/.next/static/chunks/app/api/fs/route-f2d93969c0bcf406.js +1 -0
  170. package/.next/static/chunks/app/api/git/route-f2d93969c0bcf406.js +1 -0
  171. package/.next/static/chunks/app/api/github/route-f2d93969c0bcf406.js +1 -0
  172. package/.next/static/chunks/app/api/growth/route-f2d93969c0bcf406.js +1 -0
  173. package/.next/static/chunks/app/api/health/route-f2d93969c0bcf406.js +1 -0
  174. package/.next/static/chunks/app/api/models/defaults/route-f2d93969c0bcf406.js +1 -0
  175. package/.next/static/chunks/app/api/models/route-f2d93969c0bcf406.js +1 -0
  176. package/.next/static/chunks/app/api/pi-settings/route-f2d93969c0bcf406.js +1 -0
  177. package/.next/static/chunks/app/api/plugins/route-f2d93969c0bcf406.js +1 -0
  178. package/.next/static/chunks/app/api/prompts/route-f2d93969c0bcf406.js +1 -0
  179. package/.next/static/chunks/app/api/remote/route-f2d93969c0bcf406.js +1 -0
  180. package/.next/static/chunks/app/api/security/route-f2d93969c0bcf406.js +1 -0
  181. package/.next/static/chunks/app/api/sessions/[id]/export/route-f2d93969c0bcf406.js +1 -0
  182. package/.next/static/chunks/app/api/sessions/[id]/route-f2d93969c0bcf406.js +1 -0
  183. package/.next/static/chunks/app/api/sessions/import/route-f2d93969c0bcf406.js +1 -0
  184. package/.next/static/chunks/app/api/sessions/route-f2d93969c0bcf406.js +1 -0
  185. package/.next/static/chunks/app/api/skills/route-f2d93969c0bcf406.js +1 -0
  186. package/.next/static/chunks/app/api/update/route-f2d93969c0bcf406.js +1 -0
  187. package/.next/static/chunks/app/api/version/route-f2d93969c0bcf406.js +1 -0
  188. package/.next/static/chunks/app/api/web-auth/route-f2d93969c0bcf406.js +1 -0
  189. package/.next/static/chunks/app/api/workspaces/route-f2d93969c0bcf406.js +1 -0
  190. package/.next/static/chunks/app/{layout-7f64fa67ea6ce980.js → layout-50d06e2078d6ce76.js} +1 -1
  191. package/.next/static/chunks/app/login/{page-00dbd2f395b364d5.js → page-5e276a1eeb8867df.js} +1 -1
  192. package/.next/static/chunks/app/page-71f2d3b3ed864b3d.js +1 -0
  193. package/.next/static/chunks/main-5403f0764579e5fe.js +5 -0
  194. package/.next/static/chunks/{main-app-56863ace24eb152f.js → main-app-e9b53f0e1c6591b2.js} +1 -1
  195. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f2d93969c0bcf406.js +1 -0
  196. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f2d93969c0bcf406.js +1 -0
  197. package/.next/static/chunks/next/dist/client/components/builtin/global-error-a7e260f8cc2582d7.js +1 -0
  198. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f2d93969c0bcf406.js +1 -0
  199. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f2d93969c0bcf406.js +1 -0
  200. package/.next/static/chunks/webpack-ffc09b5fcc8aaa6d.js +1 -0
  201. package/.next/static/css/7e3caf797e4cb19a.css +1 -0
  202. package/.next/types/app/api/agent/[id]/events/route.ts +1 -1
  203. package/.next/types/app/api/agent/[id]/route.ts +1 -1
  204. package/.next/types/app/api/agent/[id]/tree/route.ts +1 -1
  205. package/.next/types/app/api/agent/new/route.ts +1 -1
  206. package/.next/types/app/api/agent/resources/route.ts +355 -0
  207. package/.next/types/app/api/config/route.ts +1 -1
  208. package/.next/types/app/api/files/route.ts +1 -1
  209. package/.next/types/app/api/fs/route.ts +1 -1
  210. package/.next/types/app/api/git/route.ts +1 -1
  211. package/.next/types/app/api/github/route.ts +1 -1
  212. package/.next/types/app/api/growth/route.ts +1 -1
  213. package/.next/types/app/api/health/route.ts +1 -1
  214. package/.next/types/app/api/models/defaults/route.ts +1 -1
  215. package/.next/types/app/api/models/route.ts +1 -1
  216. package/.next/types/app/api/pi-settings/route.ts +1 -1
  217. package/.next/types/app/api/plugins/route.ts +1 -1
  218. package/.next/types/app/api/prompts/route.ts +1 -1
  219. package/.next/types/app/api/remote/route.ts +1 -1
  220. package/.next/types/app/api/security/route.ts +1 -1
  221. package/.next/types/app/api/sessions/[id]/export/route.ts +1 -1
  222. package/.next/types/app/api/sessions/[id]/route.ts +1 -1
  223. package/.next/types/app/api/sessions/import/route.ts +1 -1
  224. package/.next/types/app/api/sessions/route.ts +1 -1
  225. package/.next/types/app/api/skills/route.ts +1 -1
  226. package/.next/types/app/api/update/route.ts +1 -1
  227. package/.next/types/app/api/version/route.ts +1 -1
  228. package/.next/types/app/api/web-auth/route.ts +1 -1
  229. package/.next/types/app/api/workspaces/route.ts +1 -1
  230. package/.next/types/app/layout.ts +1 -1
  231. package/.next/types/app/login/page.ts +1 -1
  232. package/.next/types/app/page.ts +1 -1
  233. package/.next/types/routes.d.ts +2 -1
  234. package/.next/types/validator.ts +10 -0
  235. package/LICENSE +1 -1
  236. package/README.md +1 -1
  237. package/bin/pi.js +0 -0
  238. package/bin/piweb.js +0 -0
  239. package/package.json +1 -1
  240. package/scripts/build-output.mjs +0 -0
  241. package/scripts/dev.mjs +0 -0
  242. package/scripts/entrypoint.mjs +0 -0
  243. package/scripts/install-build.mjs +0 -0
  244. package/scripts/launcher.mjs +0 -0
  245. package/scripts/pick-folder.ps1 +0 -0
  246. package/scripts/postinstall.mjs +0 -0
  247. package/scripts/process-runner.mjs +0 -0
  248. package/scripts/release.mjs +0 -0
  249. package/scripts/sync-file-icons.mjs +0 -0
  250. package/scripts/sync-provider-icons.mjs +0 -0
  251. package/src/app/api/agent/resources/route.ts +18 -0
  252. package/src/app/api/remote/route.ts +5 -7
  253. package/src/app/api/sessions/[id]/export/route.ts +3 -1
  254. package/src/app/globals.css +16 -0
  255. package/src/components/AppShell.tsx +68 -21
  256. package/src/components/ChatInput.tsx +98 -27
  257. package/src/components/ChatWindow.tsx +68 -11
  258. package/src/components/ContextMeter.tsx +24 -4
  259. package/src/components/ExtensionUI.tsx +9 -3
  260. package/src/components/FileViewer.tsx +24 -21
  261. package/src/components/GitPanel.tsx +449 -449
  262. package/src/components/GrowthTree.tsx +3 -1
  263. package/src/components/ImageViewer.tsx +72 -0
  264. package/src/components/MarkdownBlock.tsx +6 -3
  265. package/src/components/PreviewImage.tsx +33 -0
  266. package/src/components/ProjectPanel.tsx +5 -3
  267. package/src/components/PromptPanel.tsx +322 -313
  268. package/src/components/RemoteSection.tsx +2 -1
  269. package/src/components/SessionSidebar.tsx +22 -4
  270. package/src/components/SettingsPanel.tsx +2654 -2639
  271. package/src/components/composer/ComposerImages.tsx +1 -79
  272. package/src/hooks/usePiWeb.ts +47 -2
  273. package/src/i18n.tsx +24 -0
  274. package/src/lib/agent-manager.ts +275 -18
  275. package/src/lib/chat/skill-expansion.ts +10 -0
  276. package/src/lib/extension-ui.ts +25 -1
  277. package/src/lib/git-service.ts +291 -289
  278. package/src/lib/github-identity.ts +245 -245
  279. package/src/lib/growth-turns.ts +3 -1
  280. package/src/lib/session-reader.ts +25 -13
  281. package/src/lib/session-retention.ts +11 -8
  282. package/src/lib/slash-command-token.ts +14 -0
  283. package/src/lib/trajectory.ts +17 -6
  284. package/src/lib/types.ts +24 -2
  285. package/src/lib/workspace-store.ts +36 -1
  286. package/.next/diagnostics/build-diagnostics.json +0 -6
  287. package/.next/diagnostics/framework.json +0 -1
  288. package/.next/server/chunks/229.js +0 -1
  289. package/.next/server/chunks/393.js +0 -1
  290. package/.next/server/chunks/612.js +0 -1
  291. package/.next/server/chunks/662.js +0 -2
  292. package/.next/server/chunks/686.js +0 -1
  293. package/.next/server/chunks/802.js +0 -2
  294. package/.next/server/chunks/807.js +0 -1
  295. package/.next/server/chunks/924.js +0 -6
  296. package/.next/static/4snUelZTEIiQYCkB5nTW7/_buildManifest.js +0 -1
  297. package/.next/static/chunks/1966.1560d0f43ac9a41a.js +0 -1
  298. package/.next/static/chunks/2577.eebf2cf291a811ad.js +0 -1
  299. package/.next/static/chunks/2716.a5228b0eeedbe4c8.js +0 -1
  300. package/.next/static/chunks/2906.546a64377e12dfbc.js +0 -1
  301. package/.next/static/chunks/3899.55a0943bf5e7fec7.js +0 -1
  302. package/.next/static/chunks/4673.8eff281cb63e2799.js +0 -1
  303. package/.next/static/chunks/4686-f03275819a719fd8.js +0 -1
  304. package/.next/static/chunks/4778.2dae0c6a9e396326.js +0 -1
  305. package/.next/static/chunks/app/_global-error/page-f977af7f858990a2.js +0 -1
  306. package/.next/static/chunks/app/_not-found/page-851dc3883a1937aa.js +0 -1
  307. package/.next/static/chunks/app/api/agent/[id]/events/route-f977af7f858990a2.js +0 -1
  308. package/.next/static/chunks/app/api/agent/[id]/route-f977af7f858990a2.js +0 -1
  309. package/.next/static/chunks/app/api/agent/[id]/tree/route-f977af7f858990a2.js +0 -1
  310. package/.next/static/chunks/app/api/agent/new/route-f977af7f858990a2.js +0 -1
  311. package/.next/static/chunks/app/api/config/route-f977af7f858990a2.js +0 -1
  312. package/.next/static/chunks/app/api/files/route-f977af7f858990a2.js +0 -1
  313. package/.next/static/chunks/app/api/fs/route-f977af7f858990a2.js +0 -1
  314. package/.next/static/chunks/app/api/git/route-f977af7f858990a2.js +0 -1
  315. package/.next/static/chunks/app/api/github/route-f977af7f858990a2.js +0 -1
  316. package/.next/static/chunks/app/api/growth/route-f977af7f858990a2.js +0 -1
  317. package/.next/static/chunks/app/api/health/route-f977af7f858990a2.js +0 -1
  318. package/.next/static/chunks/app/api/models/defaults/route-f977af7f858990a2.js +0 -1
  319. package/.next/static/chunks/app/api/models/route-f977af7f858990a2.js +0 -1
  320. package/.next/static/chunks/app/api/pi-settings/route-f977af7f858990a2.js +0 -1
  321. package/.next/static/chunks/app/api/plugins/route-f977af7f858990a2.js +0 -1
  322. package/.next/static/chunks/app/api/prompts/route-f977af7f858990a2.js +0 -1
  323. package/.next/static/chunks/app/api/remote/route-f977af7f858990a2.js +0 -1
  324. package/.next/static/chunks/app/api/security/route-f977af7f858990a2.js +0 -1
  325. package/.next/static/chunks/app/api/sessions/[id]/export/route-f977af7f858990a2.js +0 -1
  326. package/.next/static/chunks/app/api/sessions/[id]/route-f977af7f858990a2.js +0 -1
  327. package/.next/static/chunks/app/api/sessions/import/route-f977af7f858990a2.js +0 -1
  328. package/.next/static/chunks/app/api/sessions/route-f977af7f858990a2.js +0 -1
  329. package/.next/static/chunks/app/api/skills/route-f977af7f858990a2.js +0 -1
  330. package/.next/static/chunks/app/api/update/route-f977af7f858990a2.js +0 -1
  331. package/.next/static/chunks/app/api/version/route-f977af7f858990a2.js +0 -1
  332. package/.next/static/chunks/app/api/web-auth/route-f977af7f858990a2.js +0 -1
  333. package/.next/static/chunks/app/api/workspaces/route-f977af7f858990a2.js +0 -1
  334. package/.next/static/chunks/app/page-0159987118376f42.js +0 -1
  335. package/.next/static/chunks/main-22ccebc2da4d291e.js +0 -5
  336. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f977af7f858990a2.js +0 -1
  337. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f977af7f858990a2.js +0 -1
  338. package/.next/static/chunks/next/dist/client/components/builtin/global-error-c37b7707077f50ce.js +0 -1
  339. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f977af7f858990a2.js +0 -1
  340. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f977af7f858990a2.js +0 -1
  341. package/.next/static/chunks/webpack-c1b43c269867b64f.js +0 -1
  342. package/.next/static/css/93ed3daa6f3fbc35.css +0 -1
  343. package/.next/trace +0 -58
  344. package/.next/trace-build +0 -1
  345. /package/.next/static/{4snUelZTEIiQYCkB5nTW7 → QJCtqgDKzGS2BdKABd9ch}/_ssgManifest.js +0 -0
@@ -1,289 +1,291 @@
1
- /**
2
- * git-service:会话工作区的 Git 状态与差异(详情栏「Git」面板)。
3
- * 只读:porcelain v2 状态、最近提交、单文件差异、单次提交内容;不做任何写操作。
4
- * 所有 git 命令以仓库根为 cwd 运行,因为 porcelain 输出的路径都相对仓库根。
5
- */
6
- import { execFile } from "node:child_process";
7
- import fs from "node:fs/promises";
8
- import path from "node:path";
9
- import { promisify } from "node:util";
10
- import { parseNumstat, resolveGitBin } from "./growth-service";
11
- import { BoundaryError, isPathInside, resolveWorkspacePath } from "./path-security";
12
-
13
- const exec = promisify(execFile);
14
-
15
- export interface GitFileEntry {
16
- path: string;
17
- /** 暂存区状态字母(" " 表示无) */
18
- indexStatus: string;
19
- /** 工作区状态字母(" " 表示无) */
20
- workStatus: string;
21
- kind: "modified" | "added" | "deleted" | "renamed" | "untracked" | "conflict";
22
- }
23
-
24
- export interface GitCommit {
25
- hash: string;
26
- subject: string;
27
- author: string;
28
- /** git 的相对时间("2 hours ago") */
29
- relative: string;
30
- }
31
-
32
- export interface GitInfo {
33
- /** 机器上找不到 git 命令 */
34
- available: boolean;
35
- isRepo: boolean;
36
- /** 仓库根(用于把面板里的相对路径映射回文件面板) */
37
- root: string | null;
38
- branch: string | null;
39
- upstream: string | null;
40
- ahead: number;
41
- behind: number;
42
- detached: boolean;
43
- files: GitFileEntry[];
44
- /** 状态读取失败时文件变更未知;不能把空 files 解读为工作区干净。 */
45
- statusError?: string;
46
- commits: GitCommit[];
47
- }
48
-
49
- const EMPTY: GitInfo = {
50
- available: true,
51
- isRepo: false,
52
- root: null,
53
- branch: null,
54
- upstream: null,
55
- ahead: 0,
56
- behind: 0,
57
- detached: false,
58
- files: [],
59
- commits: [],
60
- };
61
-
62
- /** 单次差异输出上限:超过则截断并标记(详情栏不是完整的 diff 工具) */
63
- const MAX_DIFF_BYTES = 400 * 1024;
64
-
65
- /** 与生长记录共用解析出的 git(macOS 从 GUI / launchd 启动时 PATH 里常常没有 Homebrew 的 git) */
66
- async function runGit(cwd: string, args: string[], maxBuffer = 4 * 1024 * 1024): Promise<string> {
67
- return exec(await resolveGitBin(), args, { cwd, timeout: 15_000, maxBuffer, windowsHide: true, env: { ...process.env, GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" } }).then((r) => r.stdout);
68
- }
69
-
70
- function kindOf(x: string, y: string): GitFileEntry["kind"] {
71
- if (x === "U" || y === "U" || (x === "A" && y === "A") || (x === "D" && y === "D")) return "conflict";
72
- const letter = x !== "." ? x : y;
73
- if (letter === "A") return "added";
74
- if (letter === "D") return "deleted";
75
- if (letter === "R" || letter === "C") return "renamed";
76
- return "modified";
77
- }
78
-
79
- /** 解析仓库根(GitHub 身份解析也要用,所以导出) */
80
- export async function repoRoot(cwd: string): Promise<{ available: boolean; root: string | null }> {
81
- try {
82
- const out = (await runGit(cwd, ["rev-parse", "--show-toplevel"])).trim();
83
- return { available: true, root: out ? path.resolve(out) : null };
84
- } catch (error) {
85
- const code = (error as NodeJS.ErrnoException).code;
86
- if (code === "ENOENT") return { available: false, root: null };
87
- // exit 128:不是仓库
88
- return { available: true, root: null };
89
- }
90
- }
91
-
92
- export async function gitInfo(cwdValue: unknown): Promise<GitInfo> {
93
- const cwd = await resolveWorkspacePath(cwdValue);
94
- const { available, root } = await repoRoot(cwd);
95
- if (!available) return { ...EMPTY, available: false };
96
- if (!root) return EMPTY;
97
-
98
- let branch: string | null = null;
99
- let upstream: string | null = null;
100
- let ahead = 0;
101
- let behind = 0;
102
- let detached = false;
103
- let statusError: string | undefined;
104
- const files: GitFileEntry[] = [];
105
- try {
106
- // porcelain v2 是结构化输出,不依赖 "No branch" 这类措辞;-z 让含空格/非 ASCII 的路径也稳定
107
- const status = await runGit(root, ["status", "--porcelain=v2", "--branch", "-z"]);
108
- const records = status.split("\0");
109
- for (let i = 0; i < records.length; i += 1) {
110
- const line = records[i];
111
- if (!line) continue;
112
- if (line.startsWith("# branch.head ")) {
113
- const head = line.slice("# branch.head ".length);
114
- if (head === "(detached)") detached = true;
115
- else branch = head;
116
- continue;
117
- }
118
- if (line.startsWith("# branch.upstream ")) {
119
- upstream = line.slice("# branch.upstream ".length);
120
- continue;
121
- }
122
- if (line.startsWith("# branch.ab ")) {
123
- const m = line.match(/\+(\d+) -(\d+)/);
124
- if (m) {
125
- ahead = Number(m[1]);
126
- behind = Number(m[2]);
127
- }
128
- continue;
129
- }
130
- if (line.startsWith("# ")) continue;
131
- const type = line[0];
132
- if (type === "?") {
133
- files.push({ path: line.slice(2), indexStatus: " ", workStatus: "?", kind: "untracked" });
134
- continue;
135
- }
136
- if (type === "!") continue;
137
- if (type === "1" || type === "2" || type === "u") {
138
- const parts = line.split(" ");
139
- const xy = parts[1] ?? "..";
140
- const x = xy[0] ?? ".";
141
- const y = xy[1] ?? ".";
142
- // 1: <XY> <sub> <mH> <mI> <mW> <hH> <hI> <path>
143
- // 2: <XY> <sub> <mH> <mI> <mW> <hH> <hI> <X><score> <path> (NUL 后跟原路径,占下一条记录)
144
- // u: <XY> <sub> <m1> <m2> <m3> <mW> <h1> <h2> <h3> <path>
145
- const pathIndex = type === "1" ? 8 : type === "2" ? 9 : 10;
146
- const filePath = parts.slice(pathIndex).join(" ");
147
- if (type === "2") i += 1; // 跳过 rename 的原路径记录
148
- if (!filePath) continue;
149
- files.push({
150
- path: filePath,
151
- indexStatus: x === "." ? " " : x,
152
- workStatus: y === "." ? " " : y,
153
- kind: kindOf(x, y),
154
- });
155
- }
156
- }
157
- } catch (error) {
158
- // 分支与提交可继续展示,但文件状态未知,绝不能回退为「工作区干净」。
159
- const stderr = (error as { stderr?: unknown })?.stderr;
160
- statusError = (typeof stderr === "string" ? stderr.trim() : error instanceof Error ? error.message : String(error)) || "git status failed";
161
- }
162
-
163
- let commits: GitCommit[] = [];
164
- try {
165
- const log = await runGit(root, ["log", "-10", "--pretty=%h%x1f%s%x1f%an%x1f%cr"]);
166
- commits = log
167
- .split("\n")
168
- .filter(Boolean)
169
- .map((line) => {
170
- const [hash, subject, author, relative] = line.split("\x1f");
171
- return { hash, subject: subject ?? "", author: author ?? "", relative: relative ?? "" };
172
- });
173
- } catch {
174
- // 空仓库没有提交
175
- }
176
-
177
- if (branch === null && !detached) {
178
- try {
179
- branch = (await runGit(root, ["rev-parse", "--abbrev-ref", "HEAD"])).trim() || null;
180
- } catch {
181
- /* keep null */
182
- }
183
- }
184
- return { available: true, isRepo: true, root, branch, upstream, ahead, behind, detached, files, commits, ...(statusError ? { statusError } : {}) };
185
- }
186
-
187
- export interface GitDiffResult {
188
- patch: string;
189
- truncated: boolean;
190
- binary: boolean;
191
- /** 完整 diff 的 Git numstat,独立于用于展示的 patch 截断。二进制不计行数。 */
192
- stats: { add: number; del: number };
193
- }
194
-
195
- async function fullDiffStats(root: string, args: string[], noIndex = false): Promise<{ stats: GitDiffResult["stats"]; binary: boolean }> {
196
- let raw: string;
197
- try {
198
- raw = await runGit(root, args);
199
- } catch (error) {
200
- // --no-index 的退出码 1 表示有差异,stdout 仍是有效的 numstat。
201
- const result = error as { code?: number; stdout?: string };
202
- if (!noIndex || result.code !== 1 || typeof result.stdout !== "string" || !result.stdout) throw error;
203
- raw = result.stdout;
204
- }
205
- let add = 0;
206
- let del = 0;
207
- let binary = false;
208
- for (const entry of parseNumstat(raw).values()) {
209
- add += entry.add;
210
- del += entry.del;
211
- binary ||= entry.binary;
212
- }
213
- return { stats: { add, del }, binary };
214
- }
215
-
216
- function truncate(patch: string, stats: GitDiffResult["stats"]): GitDiffResult {
217
- if (Buffer.byteLength(patch) <= MAX_DIFF_BYTES) return { patch, truncated: false, binary: false, stats };
218
- return { patch: Buffer.from(patch).subarray(0, MAX_DIFF_BYTES).toString("utf8"), truncated: true, binary: false, stats };
219
- }
220
-
221
- function assertRepoRelative(root: string, relPath: unknown): string {
222
- if (typeof relPath !== "string" || !relPath.trim()) throw new BoundaryError("missing path");
223
- const abs = path.resolve(root, relPath);
224
- if (!isPathInside(root, abs)) throw new BoundaryError("path escapes the repository");
225
- return abs;
226
- }
227
-
228
- /** 单文件差异:staged(索引 vs HEAD)、worktree(工作区 vs 索引)、untracked(整文件视为新增) */
229
- export async function gitDiff(cwdValue: unknown, relPath: unknown, mode: unknown): Promise<GitDiffResult> {
230
- const cwd = await resolveWorkspacePath(cwdValue);
231
- const { available, root } = await repoRoot(cwd);
232
- if (!available || !root) throw new BoundaryError("not a git repository");
233
- const abs = assertRepoRelative(root, relPath);
234
- const rel = path.relative(root, abs).replace(/\\/g, "/");
235
- if (mode === "untracked") {
236
- // 未跟踪文件:合成一个全新增的 patch,让前端用同一套 diff 视图展示
237
- const realRoot = await fs.realpath(root);
238
- const realFile = await fs.realpath(abs).catch(() => { throw new BoundaryError("file not found"); });
239
- if (!isPathInside(realRoot, realFile)) throw new BoundaryError("path escapes the repository");
240
- const stat = await fs.stat(realFile).catch(() => null);
241
- if (!stat?.isFile()) throw new BoundaryError("file not found");
242
- const { stats, binary } = await fullDiffStats(root, ["diff", "--no-index", "--no-ext-diff", "--no-textconv", "--numstat", "-z", "--", "/dev/null", realFile], true);
243
- if (binary) return { patch: "", truncated: false, binary: true, stats };
244
- const buffer = Buffer.alloc(Math.min(stat.size, MAX_DIFF_BYTES + 1));
245
- const handle = await fs.open(realFile, "r");
246
- let bytesRead = 0;
247
- try {
248
- while (bytesRead < buffer.length) {
249
- const read = await handle.read(buffer, bytesRead, buffer.length - bytesRead, bytesRead);
250
- if (read.bytesRead === 0) break;
251
- bytesRead += read.bytesRead;
252
- }
253
- } finally {
254
- await handle.close();
255
- }
256
- const head = buffer.subarray(0, bytesRead);
257
- const text = head.subarray(0, MAX_DIFF_BYTES).toString("utf8").replace(/\r\n/g, "\n");
258
- const lines = text.split("\n");
259
- if (lines[lines.length - 1] === "") lines.pop();
260
- const body = lines.map((l) => `+${l}`).join("\n");
261
- return {
262
- patch: `--- /dev/null\n+++ b/${rel}\n@@ -0,0 +1,${lines.length} @@\n${body}\n`,
263
- truncated: stat.size > MAX_DIFF_BYTES,
264
- binary: false,
265
- stats,
266
- };
267
- }
268
- const scope = mode === "staged" ? ["--cached"] : [];
269
- const [out, counts] = await Promise.all([
270
- runGit(root, ["diff", ...scope, "--no-color", "--no-ext-diff", "--no-textconv", "--", rel], 8 * 1024 * 1024),
271
- fullDiffStats(root, ["diff", ...scope, "--no-ext-diff", "--no-textconv", "--numstat", "-z", "--", rel]),
272
- ]);
273
- if (counts.binary) return { patch: "", truncated: false, binary: true, stats: counts.stats };
274
- return truncate(out, counts.stats);
275
- }
276
-
277
- /** 单次提交的内容(含各文件 patch) */
278
- export async function gitShow(cwdValue: unknown, hash: unknown): Promise<GitDiffResult & { title: string }> {
279
- const cwd = await resolveWorkspacePath(cwdValue);
280
- const { available, root } = await repoRoot(cwd);
281
- if (!available || !root) throw new BoundaryError("not a git repository");
282
- if (typeof hash !== "string" || !/^[0-9a-f]{4,40}$/i.test(hash)) throw new BoundaryError("invalid commit hash");
283
- const title = (await runGit(root, ["log", "-1", "--pretty=%h %s%n%an · %cr", hash])).trim();
284
- const [out, counts] = await Promise.all([
285
- runGit(root, ["show", "--no-color", "--no-ext-diff", "--no-textconv", "--format=", "--patch", hash], 8 * 1024 * 1024),
286
- fullDiffStats(root, ["show", "--no-ext-diff", "--no-textconv", "--format=", "--numstat", "-z", hash]),
287
- ]);
288
- return { title, ...truncate(out, counts.stats), binary: counts.binary && !out.includes("@@ ") };
289
- }
1
+ /**
2
+ * git-service:会话工作区的 Git 状态与差异(详情栏「Git」面板)。
3
+ * 只读:porcelain v2 状态、最近提交、单文件差异、单次提交内容;不做任何写操作。
4
+ * 所有 git 命令以仓库根为 cwd 运行,因为 porcelain 输出的路径都相对仓库根。
5
+ */
6
+ import { execFile } from "node:child_process";
7
+ import fs from "node:fs/promises";
8
+ import path from "node:path";
9
+ import { promisify } from "node:util";
10
+ import { parseNumstat, resolveGitBin } from "./growth-service";
11
+ import { BoundaryError, isPathInside, resolveWorkspacePath } from "./path-security";
12
+ import { utf8Prefix } from "./text-sanitize";
13
+
14
+ const exec = promisify(execFile);
15
+
16
+ export interface GitFileEntry {
17
+ path: string;
18
+ /** 暂存区状态字母(" " 表示无) */
19
+ indexStatus: string;
20
+ /** 工作区状态字母(" " 表示无) */
21
+ workStatus: string;
22
+ kind: "modified" | "added" | "deleted" | "renamed" | "untracked" | "conflict";
23
+ }
24
+
25
+ export interface GitCommit {
26
+ hash: string;
27
+ subject: string;
28
+ author: string;
29
+ /** git 的相对时间("2 hours ago") */
30
+ relative: string;
31
+ }
32
+
33
+ export interface GitInfo {
34
+ /** 机器上找不到 git 命令 */
35
+ available: boolean;
36
+ isRepo: boolean;
37
+ /** 仓库根(用于把面板里的相对路径映射回文件面板) */
38
+ root: string | null;
39
+ branch: string | null;
40
+ upstream: string | null;
41
+ ahead: number;
42
+ behind: number;
43
+ detached: boolean;
44
+ files: GitFileEntry[];
45
+ /** 状态读取失败时文件变更未知;不能把空 files 解读为工作区干净。 */
46
+ statusError?: string;
47
+ commits: GitCommit[];
48
+ }
49
+
50
+ const EMPTY: GitInfo = {
51
+ available: true,
52
+ isRepo: false,
53
+ root: null,
54
+ branch: null,
55
+ upstream: null,
56
+ ahead: 0,
57
+ behind: 0,
58
+ detached: false,
59
+ files: [],
60
+ commits: [],
61
+ };
62
+
63
+ /** 单次差异输出上限:超过则截断并标记(详情栏不是完整的 diff 工具) */
64
+ const MAX_DIFF_BYTES = 400 * 1024;
65
+
66
+ /** 与生长记录共用解析出的 git(macOS 从 GUI / launchd 启动时 PATH 里常常没有 Homebrew 的 git) */
67
+ async function runGit(cwd: string, args: string[], maxBuffer = 4 * 1024 * 1024): Promise<string> {
68
+ return exec(await resolveGitBin(), args, { cwd, timeout: 15_000, maxBuffer, windowsHide: true, env: { ...process.env, GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" } }).then((r) => r.stdout);
69
+ }
70
+
71
+ function kindOf(x: string, y: string): GitFileEntry["kind"] {
72
+ if (x === "U" || y === "U" || (x === "A" && y === "A") || (x === "D" && y === "D")) return "conflict";
73
+ const letter = x !== "." ? x : y;
74
+ if (letter === "A") return "added";
75
+ if (letter === "D") return "deleted";
76
+ if (letter === "R" || letter === "C") return "renamed";
77
+ return "modified";
78
+ }
79
+
80
+ /** 解析仓库根(GitHub 身份解析也要用,所以导出) */
81
+ export async function repoRoot(cwd: string): Promise<{ available: boolean; root: string | null }> {
82
+ try {
83
+ const out = (await runGit(cwd, ["rev-parse", "--show-toplevel"])).trim();
84
+ return { available: true, root: out ? path.resolve(out) : null };
85
+ } catch (error) {
86
+ const code = (error as NodeJS.ErrnoException).code;
87
+ if (code === "ENOENT") return { available: false, root: null };
88
+ // exit 128:不是仓库
89
+ return { available: true, root: null };
90
+ }
91
+ }
92
+
93
+ export async function gitInfo(cwdValue: unknown): Promise<GitInfo> {
94
+ const cwd = await resolveWorkspacePath(cwdValue);
95
+ const { available, root } = await repoRoot(cwd);
96
+ if (!available) return { ...EMPTY, available: false };
97
+ if (!root) return EMPTY;
98
+
99
+ let branch: string | null = null;
100
+ let upstream: string | null = null;
101
+ let ahead = 0;
102
+ let behind = 0;
103
+ let detached = false;
104
+ let statusError: string | undefined;
105
+ const files: GitFileEntry[] = [];
106
+ try {
107
+ // porcelain v2 是结构化输出,不依赖 "No branch" 这类措辞;-z 让含空格/非 ASCII 的路径也稳定
108
+ const status = await runGit(root, ["status", "--porcelain=v2", "--branch", "-z"]);
109
+ const records = status.split("\0");
110
+ for (let i = 0; i < records.length; i += 1) {
111
+ const line = records[i];
112
+ if (!line) continue;
113
+ if (line.startsWith("# branch.head ")) {
114
+ const head = line.slice("# branch.head ".length);
115
+ if (head === "(detached)") detached = true;
116
+ else branch = head;
117
+ continue;
118
+ }
119
+ if (line.startsWith("# branch.upstream ")) {
120
+ upstream = line.slice("# branch.upstream ".length);
121
+ continue;
122
+ }
123
+ if (line.startsWith("# branch.ab ")) {
124
+ const m = line.match(/\+(\d+) -(\d+)/);
125
+ if (m) {
126
+ ahead = Number(m[1]);
127
+ behind = Number(m[2]);
128
+ }
129
+ continue;
130
+ }
131
+ if (line.startsWith("# ")) continue;
132
+ const type = line[0];
133
+ if (type === "?") {
134
+ files.push({ path: line.slice(2), indexStatus: " ", workStatus: "?", kind: "untracked" });
135
+ continue;
136
+ }
137
+ if (type === "!") continue;
138
+ if (type === "1" || type === "2" || type === "u") {
139
+ const parts = line.split(" ");
140
+ const xy = parts[1] ?? "..";
141
+ const x = xy[0] ?? ".";
142
+ const y = xy[1] ?? ".";
143
+ // 1: <XY> <sub> <mH> <mI> <mW> <hH> <hI> <path>
144
+ // 2: <XY> <sub> <mH> <mI> <mW> <hH> <hI> <X><score> <path> (NUL 后跟原路径,占下一条记录)
145
+ // u: <XY> <sub> <m1> <m2> <m3> <mW> <h1> <h2> <h3> <path>
146
+ const pathIndex = type === "1" ? 8 : type === "2" ? 9 : 10;
147
+ const filePath = parts.slice(pathIndex).join(" ");
148
+ if (type === "2") i += 1; // 跳过 rename 的原路径记录
149
+ if (!filePath) continue;
150
+ files.push({
151
+ path: filePath,
152
+ indexStatus: x === "." ? " " : x,
153
+ workStatus: y === "." ? " " : y,
154
+ kind: kindOf(x, y),
155
+ });
156
+ }
157
+ }
158
+ } catch (error) {
159
+ // 分支与提交可继续展示,但文件状态未知,绝不能回退为「工作区干净」。
160
+ const stderr = (error as { stderr?: unknown })?.stderr;
161
+ statusError = (typeof stderr === "string" ? stderr.trim() : error instanceof Error ? error.message : String(error)) || "git status failed";
162
+ }
163
+
164
+ let commits: GitCommit[] = [];
165
+ try {
166
+ const log = await runGit(root, ["log", "-10", "--pretty=%h%x1f%s%x1f%an%x1f%cr"]);
167
+ commits = log
168
+ .split("\n")
169
+ .filter(Boolean)
170
+ .map((line) => {
171
+ const [hash, subject, author, relative] = line.split("\x1f");
172
+ return { hash, subject: subject ?? "", author: author ?? "", relative: relative ?? "" };
173
+ });
174
+ } catch {
175
+ // 空仓库没有提交
176
+ }
177
+
178
+ if (branch === null && !detached) {
179
+ try {
180
+ branch = (await runGit(root, ["rev-parse", "--abbrev-ref", "HEAD"])).trim() || null;
181
+ } catch {
182
+ /* keep null */
183
+ }
184
+ }
185
+ return { available: true, isRepo: true, root, branch, upstream, ahead, behind, detached, files, commits, ...(statusError ? { statusError } : {}) };
186
+ }
187
+
188
+ export interface GitDiffResult {
189
+ patch: string;
190
+ truncated: boolean;
191
+ binary: boolean;
192
+ /** 完整 diff 的 Git numstat,独立于用于展示的 patch 截断。二进制不计行数。 */
193
+ stats: { add: number; del: number };
194
+ }
195
+
196
+ async function fullDiffStats(root: string, args: string[], noIndex = false): Promise<{ stats: GitDiffResult["stats"]; binary: boolean }> {
197
+ let raw: string;
198
+ try {
199
+ raw = await runGit(root, args);
200
+ } catch (error) {
201
+ // --no-index 的退出码 1 表示有差异,stdout 仍是有效的 numstat。
202
+ const result = error as { code?: number; stdout?: string };
203
+ if (!noIndex || result.code !== 1 || typeof result.stdout !== "string" || !result.stdout) throw error;
204
+ raw = result.stdout;
205
+ }
206
+ let add = 0;
207
+ let del = 0;
208
+ let binary = false;
209
+ for (const entry of parseNumstat(raw).values()) {
210
+ add += entry.add;
211
+ del += entry.del;
212
+ binary ||= entry.binary;
213
+ }
214
+ return { stats: { add, del }, binary };
215
+ }
216
+
217
+ /** 按字节截断时退到字符边界:直接 subarray 会把多字节字符切成半个,patch 末尾多出一个 U+FFFD */
218
+ function truncate(patch: string, stats: GitDiffResult["stats"]): GitDiffResult {
219
+ if (Buffer.byteLength(patch) <= MAX_DIFF_BYTES) return { patch, truncated: false, binary: false, stats };
220
+ return { patch: utf8Prefix(Buffer.from(patch), MAX_DIFF_BYTES).toString("utf8"), truncated: true, binary: false, stats };
221
+ }
222
+
223
+ function assertRepoRelative(root: string, relPath: unknown): string {
224
+ if (typeof relPath !== "string" || !relPath.trim()) throw new BoundaryError("missing path");
225
+ const abs = path.resolve(root, relPath);
226
+ if (!isPathInside(root, abs)) throw new BoundaryError("path escapes the repository");
227
+ return abs;
228
+ }
229
+
230
+ /** 单文件差异:staged(索引 vs HEAD)、worktree(工作区 vs 索引)、untracked(整文件视为新增) */
231
+ export async function gitDiff(cwdValue: unknown, relPath: unknown, mode: unknown): Promise<GitDiffResult> {
232
+ const cwd = await resolveWorkspacePath(cwdValue);
233
+ const { available, root } = await repoRoot(cwd);
234
+ if (!available || !root) throw new BoundaryError("not a git repository");
235
+ const abs = assertRepoRelative(root, relPath);
236
+ const rel = path.relative(root, abs).replace(/\\/g, "/");
237
+ if (mode === "untracked") {
238
+ // 未跟踪文件:合成一个全新增的 patch,让前端用同一套 diff 视图展示
239
+ const realRoot = await fs.realpath(root);
240
+ const realFile = await fs.realpath(abs).catch(() => { throw new BoundaryError("file not found"); });
241
+ if (!isPathInside(realRoot, realFile)) throw new BoundaryError("path escapes the repository");
242
+ const stat = await fs.stat(realFile).catch(() => null);
243
+ if (!stat?.isFile()) throw new BoundaryError("file not found");
244
+ const { stats, binary } = await fullDiffStats(root, ["diff", "--no-index", "--no-ext-diff", "--no-textconv", "--numstat", "-z", "--", "/dev/null", realFile], true);
245
+ if (binary) return { patch: "", truncated: false, binary: true, stats };
246
+ const buffer = Buffer.alloc(Math.min(stat.size, MAX_DIFF_BYTES + 1));
247
+ const handle = await fs.open(realFile, "r");
248
+ let bytesRead = 0;
249
+ try {
250
+ while (bytesRead < buffer.length) {
251
+ const read = await handle.read(buffer, bytesRead, buffer.length - bytesRead, bytesRead);
252
+ if (read.bytesRead === 0) break;
253
+ bytesRead += read.bytesRead;
254
+ }
255
+ } finally {
256
+ await handle.close();
257
+ }
258
+ const head = buffer.subarray(0, bytesRead);
259
+ const text = utf8Prefix(head, MAX_DIFF_BYTES).toString("utf8").replace(/\r\n/g, "\n");
260
+ const lines = text.split("\n");
261
+ if (lines[lines.length - 1] === "") lines.pop();
262
+ const body = lines.map((l) => `+${l}`).join("\n");
263
+ return {
264
+ patch: `--- /dev/null\n+++ b/${rel}\n@@ -0,0 +1,${lines.length} @@\n${body}\n`,
265
+ truncated: stat.size > MAX_DIFF_BYTES,
266
+ binary: false,
267
+ stats,
268
+ };
269
+ }
270
+ const scope = mode === "staged" ? ["--cached"] : [];
271
+ const [out, counts] = await Promise.all([
272
+ runGit(root, ["diff", ...scope, "--no-color", "--no-ext-diff", "--no-textconv", "--", rel], 8 * 1024 * 1024),
273
+ fullDiffStats(root, ["diff", ...scope, "--no-ext-diff", "--no-textconv", "--numstat", "-z", "--", rel]),
274
+ ]);
275
+ if (counts.binary) return { patch: "", truncated: false, binary: true, stats: counts.stats };
276
+ return truncate(out, counts.stats);
277
+ }
278
+
279
+ /** 单次提交的内容(含各文件 patch) */
280
+ export async function gitShow(cwdValue: unknown, hash: unknown): Promise<GitDiffResult & { title: string }> {
281
+ const cwd = await resolveWorkspacePath(cwdValue);
282
+ const { available, root } = await repoRoot(cwd);
283
+ if (!available || !root) throw new BoundaryError("not a git repository");
284
+ if (typeof hash !== "string" || !/^[0-9a-f]{4,40}$/i.test(hash)) throw new BoundaryError("invalid commit hash");
285
+ const title = (await runGit(root, ["log", "-1", "--pretty=%h %s%n%an · %cr", hash])).trim();
286
+ const [out, counts] = await Promise.all([
287
+ runGit(root, ["show", "--no-color", "--no-ext-diff", "--no-textconv", "--format=", "--patch", hash], 8 * 1024 * 1024),
288
+ fullDiffStats(root, ["show", "--no-ext-diff", "--no-textconv", "--format=", "--numstat", "-z", hash]),
289
+ ]);
290
+ return { title, ...truncate(out, counts.stats), binary: counts.binary && !out.includes("@@ ") };
291
+ }