@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,449 +1,449 @@
1
- "use client";
2
-
3
- /**
4
- * Git 面板(详情栏):分支 / 上游 / ahead-behind、按状态分组的变更文件、最近提交;
5
- * 点文件看差异(暂存 / 工作区 / 未跟踪整文件),点提交看该次提交的内容。
6
- * 只读——提交、推送走对话让 pi 做(「让 pi 提交」会把请求填进输入框)。
7
- */
8
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
9
- import { DiffView, diffStats, parseUnifiedDiff } from "@/components/DiffView";
10
- import { languageForPath } from "@/lib/highlight";
11
- import { IconChevronLeft14, IconCloseOutline14, IconFileOutline16, IconGitOutline16, IconRefreshOutline14 } from "@/components/icons";
12
- import { useI18n } from "@/i18n";
13
-
14
- interface GitFileEntry {
15
- path: string;
16
- indexStatus: string;
17
- workStatus: string;
18
- kind: "modified" | "added" | "deleted" | "renamed" | "untracked" | "conflict";
19
- }
20
-
21
- interface GitCommit {
22
- hash: string;
23
- subject: string;
24
- author: string;
25
- relative: string;
26
- }
27
-
28
- interface GitInfo {
29
- available: boolean;
30
- isRepo: boolean;
31
- root: string | null;
32
- branch: string | null;
33
- upstream: string | null;
34
- ahead: number;
35
- behind: number;
36
- detached: boolean;
37
- files: GitFileEntry[];
38
- commits: GitCommit[];
39
- statusError?: string;
40
- }
41
-
42
- interface GithubIdentity {
43
- login: string;
44
- name?: string;
45
- avatar?: string;
46
- url?: string;
47
- repo?: string;
48
- }
49
-
50
- interface DiffResult {
51
- patch: string;
52
- truncated: boolean;
53
- binary: boolean;
54
- stats?: { add: number; del: number };
55
- title?: string;
56
- }
57
-
58
- type View = { kind: "file"; path: string; mode: "staged" | "worktree" | "untracked"; label: string } | { kind: "commit"; hash: string };
59
-
60
- const KIND_COLOR: Record<GitFileEntry["kind"], string> = {
61
- modified: "var(--dsw-warn)",
62
- added: "var(--dsw-success)",
63
- deleted: "var(--dsw-danger)",
64
- renamed: "var(--dsw-accent)",
65
- untracked: "var(--dsw-label-caption)",
66
- conflict: "var(--dsw-danger)",
67
- };
68
-
69
- export function GitPanel({
70
- cwd,
71
- onClose,
72
- refreshKey = 0,
73
- onAskCommit,
74
- onOpenFile,
75
- }: {
76
- cwd: string;
77
- onClose: () => void;
78
- /** 外部触发刷新(回合结束 / pi 改了文件) */
79
- refreshKey?: number;
80
- /** 把「请提交当前变更」填进输入框 */
81
- onAskCommit?: () => void;
82
- /** 在本机编辑器打开(相对仓库根的路径) */
83
- onOpenFile?: (absPath: string) => void;
84
- }) {
85
- const { t } = useI18n();
86
- const [info, setInfo] = useState<GitInfo | null>(null);
87
- const [identity, setIdentity] = useState<GithubIdentity | null>(null);
88
- const [loading, setLoading] = useState(false);
89
- const [statusError, setStatusError] = useState("");
90
- const [diffError, setDiffError] = useState("");
91
- const [view, setView] = useState<View | null>(null);
92
- const [diff, setDiff] = useState<DiffResult | null>(null);
93
- const [diffLoading, setDiffLoading] = useState(false);
94
- const statusRequest = useRef<{ version: number; controller?: AbortController }>({ version: 0 });
95
- const diffRequest = useRef<{ version: number; controller?: AbortController }>({ version: 0 });
96
-
97
- const load = useCallback(async () => {
98
- statusRequest.current.controller?.abort();
99
- const controller = new AbortController();
100
- const version = ++statusRequest.current.version;
101
- statusRequest.current.controller = controller;
102
- const isCurrent = () => !controller.signal.aborted && statusRequest.current.version === version;
103
- if (!cwd) return;
104
- setLoading(true);
105
- setStatusError("");
106
- try {
107
- const r = await fetch(`/api/git?cwd=${encodeURIComponent(cwd)}`, { signal: controller.signal });
108
- const j = await r.json();
109
- if (!r.ok || !j.success) throw new Error(j.error || "failed to load git status");
110
- if (isCurrent()) setInfo(j.data as GitInfo);
111
- } catch (e) {
112
- if (isCurrent()) setStatusError(e instanceof Error ? e.message : "failed to load git status");
113
- } finally {
114
- if (isCurrent()) setLoading(false);
115
- }
116
- }, [cwd]);
117
-
118
- // GitHub 身份单独拉:要走网络(gh / 头像),不能拖慢上面那条纯本地的状态读取
119
- useEffect(() => {
120
- setIdentity(null);
121
- if (!cwd) return;
122
- const controller = new AbortController();
123
- void (async () => {
124
- try {
125
- const r = await fetch(`/api/github?cwd=${encodeURIComponent(cwd)}`, { signal: controller.signal });
126
- const j = await r.json();
127
- if (j?.success && j.data?.identity) setIdentity(j.data.identity as GithubIdentity);
128
- } catch {
129
- /* 取不到就只显示分支,不报错 */
130
- }
131
- })();
132
- return () => controller.abort();
133
- }, [cwd, refreshKey]);
134
-
135
- useEffect(() => {
136
- setInfo(null);
137
- setView(null);
138
- setDiff(null);
139
- setDiffError("");
140
- setStatusError("");
141
- setLoading(false);
142
- return () => {
143
- statusRequest.current.controller?.abort();
144
- statusRequest.current.version += 1;
145
- diffRequest.current.controller?.abort();
146
- diffRequest.current.version += 1;
147
- };
148
- }, [cwd]);
149
- useEffect(() => { void load(); }, [load, refreshKey]);
150
-
151
- const loadDiff = useCallback(async (target: View) => {
152
- diffRequest.current.controller?.abort();
153
- const controller = new AbortController();
154
- const version = ++diffRequest.current.version;
155
- diffRequest.current.controller = controller;
156
- const isCurrent = () => !controller.signal.aborted && diffRequest.current.version === version;
157
- setDiffLoading(true);
158
- setDiff(null);
159
- setDiffError("");
160
- try {
161
- const url = target.kind === "file"
162
- ? `/api/git?cwd=${encodeURIComponent(cwd)}&diff=${encodeURIComponent(target.path)}&mode=${target.mode}`
163
- : `/api/git?cwd=${encodeURIComponent(cwd)}&show=${encodeURIComponent(target.hash)}`;
164
- const r = await fetch(url, { signal: controller.signal });
165
- const j = await r.json();
166
- if (!r.ok || !j.success) throw new Error(j.error || "failed to load diff");
167
- if (isCurrent()) setDiff(j.data as DiffResult);
168
- } catch (e) {
169
- if (isCurrent()) setDiffError(e instanceof Error ? e.message : "failed to load diff");
170
- } finally {
171
- if (isCurrent()) setDiffLoading(false);
172
- }
173
- }, [cwd]);
174
-
175
- useEffect(() => {
176
- setDiff(null);
177
- setDiffError("");
178
- setDiffLoading(false);
179
- if (view) void loadDiff(view);
180
- return () => {
181
- diffRequest.current.controller?.abort();
182
- diffRequest.current.version += 1;
183
- };
184
- }, [view, loadDiff, refreshKey]);
185
- const changeView = (next: View | null) => {
186
- diffRequest.current.controller?.abort();
187
- diffRequest.current.version += 1;
188
- setDiff(null);
189
- setDiffError("");
190
- setView(next);
191
- };
192
- const error = view ? diffError : statusError || info?.statusError || "";
193
-
194
- const parsed = useMemo(() => (diff?.patch ? parseUnifiedDiff(diff.patch) : null), [diff]);
195
- // 新服务端给完整 Git 统计;旧服务端只在 patch 完整时才允许本地计数。
196
- const stats = diff?.binary ? null : diff?.stats ?? (parsed && !diff?.truncated ? diffStats(parsed) : null);
197
-
198
- const conflicts = info?.files.filter((f) => f.kind === "conflict") ?? [];
199
- const staged = info?.files.filter((f) => f.kind !== "conflict" && f.kind !== "untracked" && f.indexStatus !== " ") ?? [];
200
- const unstaged = info?.files.filter((f) => f.kind !== "conflict" && f.kind !== "untracked" && f.workStatus !== " ") ?? [];
201
- const untracked = info?.files.filter((f) => f.kind === "untracked") ?? [];
202
-
203
- /**
204
- * 按 Git 的三个区呈现(分支 / 上游那些是「版本库」侧,在别的行):
205
- * - 暂存区:`git add` 过的,下次 commit 会带上;
206
- * - 工作区:改了还没 add 的 + 未跟踪的新文件(都属于「还没进暂存区」);
207
- * - 冲突单独置顶(不先解决,提交走不下去)。
208
- * 未跟踪文件要整份当新增行看,模式逐文件给,不能按组统一。
209
- */
210
- const areas: Array<{ key: string; label: string; entries: Array<{ file: GitFileEntry; mode: "staged" | "worktree" | "untracked" }>; danger?: boolean }> = [
211
- { key: "conflict", label: t.gitConflict, entries: conflicts.map((file) => ({ file, mode: "worktree" as const })), danger: true },
212
- { key: "staged", label: t.gitStagingArea, entries: staged.map((file) => ({ file, mode: "staged" as const })) },
213
- {
214
- key: "worktree",
215
- label: t.gitWorkingTree,
216
- entries: [...unstaged.map((file) => ({ file, mode: "worktree" as const })), ...untracked.map((file) => ({ file, mode: "untracked" as const }))],
217
- },
218
- ].filter((area) => area.entries.length > 0);
219
-
220
- const openFile = (f: GitFileEntry, mode: "staged" | "worktree" | "untracked", label: string) => changeView({ kind: "file", path: f.path, mode, label });
221
-
222
- const headerTitle = view
223
- ? view.kind === "file"
224
- ? view.path.split("/").pop() ?? view.path
225
- : diff?.title?.split("\n")[0] ?? view.hash
226
- : t.gitPanel;
227
-
228
- return (
229
- <div className="flex h-full min-h-0 flex-col p-4">
230
- <div className="mb-3 flex items-center gap-2">
231
- {view ? (
232
- <button className="icon-btn" style={{ width: 22, height: 22 }} title={t.gitBack} onClick={() => changeView(null)}>
233
- <IconChevronLeft14 size={13} />
234
- </button>
235
- ) : (
236
- <IconGitOutline16 size={15} style={{ flex: "none" }} />
237
- )}
238
- <span className="min-w-0 flex-1 truncate" style={{ fontSize: 13, fontWeight: 600 }} title={view?.kind === "file" ? view.path : undefined}>
239
- {headerTitle}
240
- </span>
241
- {view?.kind === "file" && onOpenFile && info?.root && (
242
- <button className="icon-btn" style={{ width: 22, height: 22 }} title={t.fileOpenEditor} onClick={() => onOpenFile(`${info.root}/${view.path}`)}>
243
- <IconFileOutline16 size={13} />
244
- </button>
245
- )}
246
- <button className="icon-btn" style={{ width: 22, height: 22 }} title={t.panelRefresh} onClick={() => (view ? void loadDiff(view) : void load())}>
247
- <IconRefreshOutline14 size={13} />
248
- </button>
249
- <button className="icon-btn" style={{ width: 22, height: 22 }} title={t.close} onClick={onClose}>
250
- <IconCloseOutline14 size={13} />
251
- </button>
252
- </div>
253
-
254
- {error && (
255
- <div className="mb-2 rounded-xl px-3 py-2" style={{ fontSize: 12.5, background: "var(--dsw-danger)", color: "white" }} role="alert">
256
- {!view && <div>{t.gitStatusUnknown}</div>}
257
- {error}
258
- </div>
259
- )}
260
-
261
- <div className="min-h-0 flex-1 overflow-y-auto">
262
- {/* ---------- 差异视图 ---------- */}
263
- {view && (
264
- <div>
265
- <div className="mb-2 flex flex-wrap items-center gap-2" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
266
- {view.kind === "file" ? <span>{view.label}</span> : diff?.title?.split("\n")[1] ? <span>{diff.title.split("\n")[1]}</span> : null}
267
- {stats && (
268
- <span style={{ fontFamily: "var(--font-mono)" }}>
269
- <span style={{ color: "var(--dsw-success)" }}>+{stats.add}</span> <span style={{ color: "var(--dsw-danger)" }}>−{stats.del}</span>
270
- </span>
271
- )}
272
- {diff?.truncated && <span style={{ color: "var(--dsw-warn)" }}>{t.gitDiffTruncated}</span>}
273
- </div>
274
- {diffLoading && !diff && <div style={{ fontSize: 12.5, color: "var(--dsw-label-caption)", padding: "4px 2px" }}>{t.panelLoading}</div>}
275
- {diff?.binary && <div style={{ fontSize: 12.5, color: "var(--dsw-label-caption)", padding: "4px 2px" }}>{t.gitBinary}</div>}
276
- {diff && !diff.binary && !parsed && <div style={{ fontSize: 12.5, color: "var(--dsw-label-caption)", padding: "4px 2px" }}>{t.gitDiffEmpty}</div>}
277
- {parsed && (
278
- <div className="overflow-x-auto rounded-xl px-2 py-1.5" style={{ background: "var(--dsw-hover)" }}>
279
- <DiffView lines={parsed} language={view?.kind === "file" ? languageForPath(view.path) : undefined} />
280
- </div>
281
- )}
282
- </div>
283
- )}
284
-
285
- {/* ---------- 状态列表 ---------- */}
286
- {!view && loading && !info && <div style={{ fontSize: 12.5, color: "var(--dsw-label-caption)", padding: "4px 2px" }}>{t.panelLoading}</div>}
287
- {!view && !loading && info && !info.available && (
288
- <div style={{ fontSize: 12.5, color: "var(--dsw-danger)", padding: "8px 2px", lineHeight: 1.6 }}>{t.gitUnavailable}</div>
289
- )}
290
- {!view && !loading && info && info.available && !info.isRepo && (
291
- <div style={{ fontSize: 12.5, color: "var(--dsw-label-caption)", padding: "8px 2px", lineHeight: 1.6 }}>{t.gitNotRepo}</div>
292
- )}
293
- {!view && info?.isRepo && (
294
- <div className="flex flex-col gap-4">
295
- {/* GitHub 身份:头像 + 账号(取不到就整行不出现) */}
296
- {identity && (
297
- <div className="flex items-center gap-2">
298
- {identity.avatar ? (
299
- // eslint-disable-next-line @next/next/no-img-element
300
- <img src={identity.avatar} alt="" width={22} height={22} className="flex-none rounded-full" />
301
- ) : (
302
- <span
303
- className="grid flex-none place-items-center rounded-full"
304
- style={{ width: 22, height: 22, background: "var(--dsw-selector)", fontSize: 11, fontWeight: 600, color: "var(--dsw-label-secondary)" }}
305
- aria-hidden
306
- >
307
- {identity.login.slice(0, 1).toUpperCase()}
308
- </span>
309
- )}
310
- <a
311
- href={identity.url ?? `https://github.com/${identity.login}`}
312
- target="_blank"
313
- rel="noreferrer"
314
- className="min-w-0 truncate"
315
- style={{ fontSize: 12.5, color: "var(--dsw-label-secondary)", textDecoration: "none" }}
316
- title={identity.name ? `${identity.name} (@${identity.login})` : `@${identity.login}`}
317
- >
318
- @{identity.login}
319
- </a>
320
- {identity.repo && (
321
- <span className="min-w-0 truncate" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }} title={identity.repo}>
322
- · {identity.repo}
323
- </span>
324
- )}
325
- </div>
326
- )}
327
-
328
- {/* 分支行 */}
329
- <div className="flex flex-wrap items-center gap-2" style={{ fontSize: 12.5 }}>
330
- <span
331
- className="flex items-center gap-1.5 rounded-full px-2.5 py-1"
332
- style={{ background: "var(--dsw-accent-soft)", color: "var(--dsw-accent)", fontWeight: 600, border: "1px solid var(--dsw-border-l2)" }}
333
- >
334
- <IconGitOutline16 size={12} />
335
- {info.detached ? t.gitDetached : info.branch ?? "—"}
336
- </span>
337
- {info.ahead > 0 && <span style={{ color: "var(--dsw-label-secondary)" }}>↑ {t.gitAhead.replace("{n}", String(info.ahead))}</span>}
338
- {info.behind > 0 && <span style={{ color: "var(--dsw-label-secondary)" }}>↓ {t.gitBehind.replace("{n}", String(info.behind))}</span>}
339
- <span className="min-w-0 truncate" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }} title={info.upstream ?? undefined}>
340
- {info.upstream ? t.gitUpstream.replace("{name}", info.upstream) : info.detached ? "" : t.gitNoUpstream}
341
- </span>
342
- </div>
343
-
344
- {/* 让 pi 提交 */}
345
- {!error && info.files.length > 0 && onAskCommit && (
346
- <button
347
- type="button"
348
- className="btn-outline self-start"
349
- style={{ height: 28, padding: "0 12px", fontSize: 12 }}
350
- onClick={onAskCommit}
351
- title={t.gitAskCommitPrompt}
352
- >
353
- {t.gitAskCommit}
354
- </button>
355
- )}
356
-
357
- {/* 变更文件 */}
358
- {error ? null : info.files.length === 0 ? (
359
- <div style={{ fontSize: 12.5, color: "var(--dsw-label-caption)" }}>{t.gitNoChanges}</div>
360
- ) : (
361
- areas.map((area) => (
362
- <div key={area.key}>
363
- <div className="mb-1 flex items-center gap-2" style={{ fontSize: 11.5, color: area.danger ? "var(--dsw-danger)" : "var(--dsw-label-caption)" }}>
364
- <span style={{ fontWeight: 600 }}>{area.label}</span>
365
- <span>{area.entries.length}</span>
366
- </div>
367
- <div className="flex flex-col gap-0.5">
368
- {area.entries.map(({ file: f, mode }) => (
369
- <button
370
- key={`${area.key}:${f.path}`}
371
- type="button"
372
- className="flex w-full items-center gap-2 rounded-xl px-2.5 py-1.5 text-left transition-all"
373
- title={f.path}
374
- style={{ transition: "all 0.18s var(--isx-ease)" }}
375
- onMouseEnter={(e) => {
376
- e.currentTarget.style.background = "var(--dsw-hover)";
377
- e.currentTarget.style.transform = "translateX(2px)";
378
- }}
379
- onMouseLeave={(e) => {
380
- e.currentTarget.style.background = "transparent";
381
- e.currentTarget.style.transform = "none";
382
- }}
383
- onClick={() => openFile(f, mode, mode === "staged" ? t.gitStagedDiff : mode === "untracked" ? t.gitUntracked : t.gitWorktreeDiff)}
384
- >
385
- <span
386
- className="flex-none rounded-full px-1.5 text-center"
387
- style={{
388
- minWidth: 18,
389
- fontSize: 10,
390
- fontFamily: "var(--font-mono)",
391
- fontWeight: 700,
392
- color: KIND_COLOR[f.kind],
393
- background: "var(--dsw-hover)",
394
- border: "1px solid currentColor",
395
- }}
396
- >
397
- {f.kind === "untracked" ? "?" : f.kind === "conflict" ? "!" : mode === "staged" ? f.indexStatus : f.workStatus !== " " ? f.workStatus : f.indexStatus}
398
- </span>
399
- <span className="min-w-0 flex-1 truncate" style={{ fontSize: 12, fontFamily: "var(--font-mono)", color: "var(--dsw-label-secondary)" }}>
400
- {f.path}
401
- </span>
402
- </button>
403
- ))}
404
- </div>
405
- </div>
406
- ))
407
- )}
408
-
409
- {/* 最近提交 */}
410
- <div className="hairline-t pt-3">
411
- <div className="mb-1" style={{ fontSize: 11.5, fontWeight: 600, color: "var(--dsw-label-caption)" }}>{t.gitRecentCommits}</div>
412
- {info.commits.length === 0 ? (
413
- <div style={{ fontSize: 12.5, color: "var(--dsw-label-caption)", padding: "4px 2px" }}>{t.gitNoCommits}</div>
414
- ) : (
415
- <div className="flex flex-col gap-0.5">
416
- {info.commits.map((c) => (
417
- <button
418
- key={c.hash}
419
- type="button"
420
- className="flex w-full flex-col items-stretch rounded-xl px-2.5 py-1.5 text-left transition-all"
421
- style={{ transition: "all 0.18s var(--isx-ease)" }}
422
- onMouseEnter={(e) => {
423
- e.currentTarget.style.background = "var(--dsw-hover)";
424
- e.currentTarget.style.transform = "translateX(2px)";
425
- }}
426
- onMouseLeave={(e) => {
427
- e.currentTarget.style.background = "transparent";
428
- e.currentTarget.style.transform = "none";
429
- }}
430
- onClick={() => changeView({ kind: "commit", hash: c.hash })}
431
- >
432
- <span className="flex items-baseline gap-2">
433
- <span className="flex-none rounded-md px-1.5 py-0.5" style={{ fontSize: 10.5, fontFamily: "var(--font-mono)", fontWeight: 600, color: "var(--dsw-accent)", background: "var(--dsw-accent-soft)" }}>{c.hash}</span>
434
- <span className="min-w-0 flex-1 truncate" style={{ fontSize: 12, color: "var(--dsw-label-secondary)" }} title={c.subject}>{c.subject}</span>
435
- </span>
436
- <span className="truncate" style={{ fontSize: 11, color: "var(--dsw-label-caption)", paddingLeft: 0, marginTop: 2 }}>
437
- {c.author} · {c.relative}
438
- </span>
439
- </button>
440
- ))}
441
- </div>
442
- )}
443
- </div>
444
- </div>
445
- )}
446
- </div>
447
- </div>
448
- );
449
- }
1
+ "use client";
2
+
3
+ /**
4
+ * Git 面板(详情栏):分支 / 上游 / ahead-behind、按状态分组的变更文件、最近提交;
5
+ * 点文件看差异(暂存 / 工作区 / 未跟踪整文件),点提交看该次提交的内容。
6
+ * 只读——提交、推送走对话让 pi 做(「让 pi 提交」会把请求填进输入框)。
7
+ */
8
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
9
+ import { DiffView, diffStats, parseUnifiedDiff } from "@/components/DiffView";
10
+ import { languageForPath } from "@/lib/highlight";
11
+ import { IconChevronLeft14, IconCloseOutline14, IconFileOutline16, IconGitOutline16, IconRefreshOutline14 } from "@/components/icons";
12
+ import { useI18n } from "@/i18n";
13
+
14
+ interface GitFileEntry {
15
+ path: string;
16
+ indexStatus: string;
17
+ workStatus: string;
18
+ kind: "modified" | "added" | "deleted" | "renamed" | "untracked" | "conflict";
19
+ }
20
+
21
+ interface GitCommit {
22
+ hash: string;
23
+ subject: string;
24
+ author: string;
25
+ relative: string;
26
+ }
27
+
28
+ interface GitInfo {
29
+ available: boolean;
30
+ isRepo: boolean;
31
+ root: string | null;
32
+ branch: string | null;
33
+ upstream: string | null;
34
+ ahead: number;
35
+ behind: number;
36
+ detached: boolean;
37
+ files: GitFileEntry[];
38
+ commits: GitCommit[];
39
+ statusError?: string;
40
+ }
41
+
42
+ interface GithubIdentity {
43
+ login: string;
44
+ name?: string;
45
+ avatar?: string;
46
+ url?: string;
47
+ repo?: string;
48
+ }
49
+
50
+ interface DiffResult {
51
+ patch: string;
52
+ truncated: boolean;
53
+ binary: boolean;
54
+ stats?: { add: number; del: number };
55
+ title?: string;
56
+ }
57
+
58
+ type View = { kind: "file"; path: string; mode: "staged" | "worktree" | "untracked"; label: string } | { kind: "commit"; hash: string };
59
+
60
+ const KIND_COLOR: Record<GitFileEntry["kind"], string> = {
61
+ modified: "var(--dsw-warn)",
62
+ added: "var(--dsw-success)",
63
+ deleted: "var(--dsw-danger)",
64
+ renamed: "var(--dsw-accent)",
65
+ untracked: "var(--dsw-label-caption)",
66
+ conflict: "var(--dsw-danger)",
67
+ };
68
+
69
+ export function GitPanel({
70
+ cwd,
71
+ onClose,
72
+ refreshKey = 0,
73
+ onAskCommit,
74
+ onOpenFile,
75
+ }: {
76
+ cwd: string;
77
+ onClose: () => void;
78
+ /** 外部触发刷新(回合结束 / pi 改了文件) */
79
+ refreshKey?: number;
80
+ /** 把「请提交当前变更」填进输入框 */
81
+ onAskCommit?: () => void;
82
+ /** 在本机编辑器打开(相对仓库根的路径) */
83
+ onOpenFile?: (absPath: string) => void;
84
+ }) {
85
+ const { t } = useI18n();
86
+ const [info, setInfo] = useState<GitInfo | null>(null);
87
+ const [identity, setIdentity] = useState<GithubIdentity | null>(null);
88
+ const [loading, setLoading] = useState(false);
89
+ const [statusError, setStatusError] = useState("");
90
+ const [diffError, setDiffError] = useState("");
91
+ const [view, setView] = useState<View | null>(null);
92
+ const [diff, setDiff] = useState<DiffResult | null>(null);
93
+ const [diffLoading, setDiffLoading] = useState(false);
94
+ const statusRequest = useRef<{ version: number; controller?: AbortController }>({ version: 0 });
95
+ const diffRequest = useRef<{ version: number; controller?: AbortController }>({ version: 0 });
96
+
97
+ const load = useCallback(async () => {
98
+ statusRequest.current.controller?.abort();
99
+ const controller = new AbortController();
100
+ const version = ++statusRequest.current.version;
101
+ statusRequest.current.controller = controller;
102
+ const isCurrent = () => !controller.signal.aborted && statusRequest.current.version === version;
103
+ if (!cwd) return;
104
+ setLoading(true);
105
+ setStatusError("");
106
+ try {
107
+ const r = await fetch(`/api/git?cwd=${encodeURIComponent(cwd)}`, { signal: controller.signal });
108
+ const j = await r.json();
109
+ if (!r.ok || !j.success) throw new Error(j.error || "failed to load git status");
110
+ if (isCurrent()) setInfo(j.data as GitInfo);
111
+ } catch (e) {
112
+ if (isCurrent()) setStatusError(e instanceof Error ? e.message : "failed to load git status");
113
+ } finally {
114
+ if (isCurrent()) setLoading(false);
115
+ }
116
+ }, [cwd]);
117
+
118
+ // GitHub 身份单独拉:要走网络(gh / 头像),不能拖慢上面那条纯本地的状态读取
119
+ useEffect(() => {
120
+ setIdentity(null);
121
+ if (!cwd) return;
122
+ const controller = new AbortController();
123
+ void (async () => {
124
+ try {
125
+ const r = await fetch(`/api/github?cwd=${encodeURIComponent(cwd)}`, { signal: controller.signal });
126
+ const j = await r.json();
127
+ if (j?.success && j.data?.identity) setIdentity(j.data.identity as GithubIdentity);
128
+ } catch {
129
+ /* 取不到就只显示分支,不报错 */
130
+ }
131
+ })();
132
+ return () => controller.abort();
133
+ }, [cwd, refreshKey]);
134
+
135
+ useEffect(() => {
136
+ setInfo(null);
137
+ setView(null);
138
+ setDiff(null);
139
+ setDiffError("");
140
+ setStatusError("");
141
+ setLoading(false);
142
+ return () => {
143
+ statusRequest.current.controller?.abort();
144
+ statusRequest.current.version += 1;
145
+ diffRequest.current.controller?.abort();
146
+ diffRequest.current.version += 1;
147
+ };
148
+ }, [cwd]);
149
+ useEffect(() => { void load(); }, [load, refreshKey]);
150
+
151
+ const loadDiff = useCallback(async (target: View) => {
152
+ diffRequest.current.controller?.abort();
153
+ const controller = new AbortController();
154
+ const version = ++diffRequest.current.version;
155
+ diffRequest.current.controller = controller;
156
+ const isCurrent = () => !controller.signal.aborted && diffRequest.current.version === version;
157
+ setDiffLoading(true);
158
+ setDiff(null);
159
+ setDiffError("");
160
+ try {
161
+ const url = target.kind === "file"
162
+ ? `/api/git?cwd=${encodeURIComponent(cwd)}&diff=${encodeURIComponent(target.path)}&mode=${target.mode}`
163
+ : `/api/git?cwd=${encodeURIComponent(cwd)}&show=${encodeURIComponent(target.hash)}`;
164
+ const r = await fetch(url, { signal: controller.signal });
165
+ const j = await r.json();
166
+ if (!r.ok || !j.success) throw new Error(j.error || "failed to load diff");
167
+ if (isCurrent()) setDiff(j.data as DiffResult);
168
+ } catch (e) {
169
+ if (isCurrent()) setDiffError(e instanceof Error ? e.message : "failed to load diff");
170
+ } finally {
171
+ if (isCurrent()) setDiffLoading(false);
172
+ }
173
+ }, [cwd]);
174
+
175
+ useEffect(() => {
176
+ setDiff(null);
177
+ setDiffError("");
178
+ setDiffLoading(false);
179
+ if (view) void loadDiff(view);
180
+ return () => {
181
+ diffRequest.current.controller?.abort();
182
+ diffRequest.current.version += 1;
183
+ };
184
+ }, [view, loadDiff, refreshKey]);
185
+ const changeView = (next: View | null) => {
186
+ diffRequest.current.controller?.abort();
187
+ diffRequest.current.version += 1;
188
+ setDiff(null);
189
+ setDiffError("");
190
+ setView(next);
191
+ };
192
+ const error = view ? diffError : statusError || info?.statusError || "";
193
+
194
+ const parsed = useMemo(() => (diff?.patch ? parseUnifiedDiff(diff.patch) : null), [diff]);
195
+ // 新服务端给完整 Git 统计;旧服务端只在 patch 完整时才允许本地计数。
196
+ const stats = diff?.binary ? null : diff?.stats ?? (parsed && !diff?.truncated ? diffStats(parsed) : null);
197
+
198
+ const conflicts = info?.files.filter((f) => f.kind === "conflict") ?? [];
199
+ const staged = info?.files.filter((f) => f.kind !== "conflict" && f.kind !== "untracked" && f.indexStatus !== " ") ?? [];
200
+ const unstaged = info?.files.filter((f) => f.kind !== "conflict" && f.kind !== "untracked" && f.workStatus !== " ") ?? [];
201
+ const untracked = info?.files.filter((f) => f.kind === "untracked") ?? [];
202
+
203
+ /**
204
+ * 按 Git 的三个区呈现(分支 / 上游那些是「版本库」侧,在别的行):
205
+ * - 暂存区:`git add` 过的,下次 commit 会带上;
206
+ * - 工作区:改了还没 add 的 + 未跟踪的新文件(都属于「还没进暂存区」);
207
+ * - 冲突单独置顶(不先解决,提交走不下去)。
208
+ * 未跟踪文件要整份当新增行看,模式逐文件给,不能按组统一。
209
+ */
210
+ const areas: Array<{ key: string; label: string; entries: Array<{ file: GitFileEntry; mode: "staged" | "worktree" | "untracked" }>; danger?: boolean }> = [
211
+ { key: "conflict", label: t.gitConflict, entries: conflicts.map((file) => ({ file, mode: "worktree" as const })), danger: true },
212
+ { key: "staged", label: t.gitStagingArea, entries: staged.map((file) => ({ file, mode: "staged" as const })) },
213
+ {
214
+ key: "worktree",
215
+ label: t.gitWorkingTree,
216
+ entries: [...unstaged.map((file) => ({ file, mode: "worktree" as const })), ...untracked.map((file) => ({ file, mode: "untracked" as const }))],
217
+ },
218
+ ].filter((area) => area.entries.length > 0);
219
+
220
+ const openFile = (f: GitFileEntry, mode: "staged" | "worktree" | "untracked", label: string) => changeView({ kind: "file", path: f.path, mode, label });
221
+
222
+ const headerTitle = view
223
+ ? view.kind === "file"
224
+ ? view.path.split("/").pop() ?? view.path
225
+ : diff?.title?.split("\n")[0] ?? view.hash
226
+ : t.gitPanel;
227
+
228
+ return (
229
+ <div className="flex h-full min-h-0 flex-col p-4">
230
+ <div className="mb-3 flex items-center gap-2">
231
+ {view ? (
232
+ <button className="icon-btn" style={{ width: 22, height: 22 }} title={t.gitBack} onClick={() => changeView(null)}>
233
+ <IconChevronLeft14 size={13} />
234
+ </button>
235
+ ) : (
236
+ <IconGitOutline16 size={15} style={{ flex: "none" }} />
237
+ )}
238
+ <span className="min-w-0 flex-1 truncate" style={{ fontSize: 13, fontWeight: 600 }} title={view?.kind === "file" ? view.path : undefined}>
239
+ {headerTitle}
240
+ </span>
241
+ {view?.kind === "file" && onOpenFile && info?.root && (
242
+ <button className="icon-btn" style={{ width: 22, height: 22 }} title={t.fileOpenEditor} onClick={() => onOpenFile(`${info.root}/${view.path}`)}>
243
+ <IconFileOutline16 size={13} />
244
+ </button>
245
+ )}
246
+ <button className="icon-btn" style={{ width: 22, height: 22 }} title={t.panelRefresh} onClick={() => (view ? void loadDiff(view) : void load())}>
247
+ <IconRefreshOutline14 size={13} />
248
+ </button>
249
+ <button className="icon-btn" style={{ width: 22, height: 22 }} title={t.close} onClick={onClose}>
250
+ <IconCloseOutline14 size={13} />
251
+ </button>
252
+ </div>
253
+
254
+ {error && (
255
+ <div className="mb-2 rounded-xl px-3 py-2" style={{ fontSize: 12.5, background: "var(--dsw-danger)", color: "white" }} role="alert">
256
+ {!view && <div>{t.gitStatusUnknown}</div>}
257
+ {error}
258
+ </div>
259
+ )}
260
+
261
+ <div className="min-h-0 flex-1 overflow-y-auto">
262
+ {/* ---------- 差异视图 ---------- */}
263
+ {view && (
264
+ <div>
265
+ <div className="mb-2 flex flex-wrap items-center gap-2" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
266
+ {view.kind === "file" ? <span>{view.label}</span> : diff?.title?.split("\n")[1] ? <span>{diff.title.split("\n")[1]}</span> : null}
267
+ {stats && (
268
+ <span style={{ fontFamily: "var(--font-mono)" }}>
269
+ <span style={{ color: "var(--dsw-success)" }}>+{stats.add}</span> <span style={{ color: "var(--dsw-danger)" }}>−{stats.del}</span>
270
+ </span>
271
+ )}
272
+ {diff?.truncated && <span style={{ color: "var(--dsw-warn)" }}>{t.gitDiffTruncated}</span>}
273
+ </div>
274
+ {diffLoading && !diff && <div style={{ fontSize: 12.5, color: "var(--dsw-label-caption)", padding: "4px 2px" }}>{t.panelLoading}</div>}
275
+ {diff?.binary && <div style={{ fontSize: 12.5, color: "var(--dsw-label-caption)", padding: "4px 2px" }}>{t.gitBinary}</div>}
276
+ {diff && !diff.binary && !parsed && <div style={{ fontSize: 12.5, color: "var(--dsw-label-caption)", padding: "4px 2px" }}>{t.gitDiffEmpty}</div>}
277
+ {parsed && (
278
+ <div className="overflow-x-auto rounded-xl px-2 py-1.5" style={{ background: "var(--dsw-hover)" }}>
279
+ <DiffView lines={parsed} language={view?.kind === "file" ? languageForPath(view.path) : undefined} />
280
+ </div>
281
+ )}
282
+ </div>
283
+ )}
284
+
285
+ {/* ---------- 状态列表 ---------- */}
286
+ {!view && loading && !info && <div style={{ fontSize: 12.5, color: "var(--dsw-label-caption)", padding: "4px 2px" }}>{t.panelLoading}</div>}
287
+ {!view && !loading && info && !info.available && (
288
+ <div style={{ fontSize: 12.5, color: "var(--dsw-danger)", padding: "8px 2px", lineHeight: 1.6 }}>{t.gitUnavailable}</div>
289
+ )}
290
+ {!view && !loading && info && info.available && !info.isRepo && (
291
+ <div style={{ fontSize: 12.5, color: "var(--dsw-label-caption)", padding: "8px 2px", lineHeight: 1.6 }}>{t.gitNotRepo}</div>
292
+ )}
293
+ {!view && info?.isRepo && (
294
+ <div className="flex flex-col gap-4">
295
+ {/* GitHub 身份:头像 + 账号(取不到就整行不出现) */}
296
+ {identity && (
297
+ <div className="flex items-center gap-2">
298
+ {identity.avatar ? (
299
+ // eslint-disable-next-line @next/next/no-img-element
300
+ <img src={identity.avatar} alt="" width={22} height={22} className="flex-none rounded-full" />
301
+ ) : (
302
+ <span
303
+ className="grid flex-none place-items-center rounded-full"
304
+ style={{ width: 22, height: 22, background: "var(--dsw-selector)", fontSize: 11, fontWeight: 600, color: "var(--dsw-label-secondary)" }}
305
+ aria-hidden
306
+ >
307
+ {identity.login.slice(0, 1).toUpperCase()}
308
+ </span>
309
+ )}
310
+ <a
311
+ href={identity.url ?? `https://github.com/${identity.login}`}
312
+ target="_blank"
313
+ rel="noreferrer"
314
+ className="min-w-0 truncate"
315
+ style={{ fontSize: 12.5, color: "var(--dsw-label-secondary)", textDecoration: "none" }}
316
+ title={identity.name ? `${identity.name} (@${identity.login})` : `@${identity.login}`}
317
+ >
318
+ @{identity.login}
319
+ </a>
320
+ {identity.repo && (
321
+ <span className="min-w-0 truncate" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }} title={identity.repo}>
322
+ · {identity.repo}
323
+ </span>
324
+ )}
325
+ </div>
326
+ )}
327
+
328
+ {/* 分支行 */}
329
+ <div className="flex flex-wrap items-center gap-2" style={{ fontSize: 12.5 }}>
330
+ <span
331
+ className="flex items-center gap-1.5 rounded-full px-2.5 py-1"
332
+ style={{ background: "var(--dsw-accent-soft)", color: "var(--dsw-accent)", fontWeight: 600, border: "1px solid var(--dsw-border-l2)" }}
333
+ >
334
+ <IconGitOutline16 size={12} />
335
+ {info.detached ? t.gitDetached : info.branch ?? "—"}
336
+ </span>
337
+ {info.ahead > 0 && <span style={{ color: "var(--dsw-label-secondary)" }}>↑ {t.gitAhead.replace("{n}", String(info.ahead))}</span>}
338
+ {info.behind > 0 && <span style={{ color: "var(--dsw-label-secondary)" }}>↓ {t.gitBehind.replace("{n}", String(info.behind))}</span>}
339
+ <span className="min-w-0 truncate" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }} title={info.upstream ?? undefined}>
340
+ {info.upstream ? t.gitUpstream.replace("{name}", info.upstream) : info.detached ? "" : t.gitNoUpstream}
341
+ </span>
342
+ </div>
343
+
344
+ {/* 让 pi 提交 */}
345
+ {!error && info.files.length > 0 && onAskCommit && (
346
+ <button
347
+ type="button"
348
+ className="btn-outline self-start"
349
+ style={{ height: 28, padding: "0 12px", fontSize: 12 }}
350
+ onClick={onAskCommit}
351
+ title={t.gitAskCommitPrompt}
352
+ >
353
+ {t.gitAskCommit}
354
+ </button>
355
+ )}
356
+
357
+ {/* 变更文件 */}
358
+ {error ? null : info.files.length === 0 ? (
359
+ <div style={{ fontSize: 12.5, color: "var(--dsw-label-caption)" }}>{t.gitNoChanges}</div>
360
+ ) : (
361
+ areas.map((area) => (
362
+ <div key={area.key}>
363
+ <div className="mb-1 flex items-center gap-2" style={{ fontSize: 11.5, color: area.danger ? "var(--dsw-danger)" : "var(--dsw-label-caption)" }}>
364
+ <span style={{ fontWeight: 600 }}>{area.label}</span>
365
+ <span>{area.entries.length}</span>
366
+ </div>
367
+ <div className="flex flex-col gap-0.5">
368
+ {area.entries.map(({ file: f, mode }) => (
369
+ <button
370
+ key={`${area.key}:${f.path}`}
371
+ type="button"
372
+ className="flex w-full items-center gap-2 rounded-xl px-2.5 py-1.5 text-left transition-all"
373
+ title={f.path}
374
+ style={{ transition: "all 0.18s var(--isx-ease)" }}
375
+ onMouseEnter={(e) => {
376
+ e.currentTarget.style.background = "var(--dsw-hover)";
377
+ e.currentTarget.style.transform = "translateX(2px)";
378
+ }}
379
+ onMouseLeave={(e) => {
380
+ e.currentTarget.style.background = "transparent";
381
+ e.currentTarget.style.transform = "none";
382
+ }}
383
+ onClick={() => openFile(f, mode, mode === "staged" ? t.gitStagedDiff : mode === "untracked" ? t.gitUntracked : t.gitWorktreeDiff)}
384
+ >
385
+ <span
386
+ className="flex-none rounded-full px-1.5 text-center"
387
+ style={{
388
+ minWidth: 18,
389
+ fontSize: 10,
390
+ fontFamily: "var(--font-mono)",
391
+ fontWeight: 700,
392
+ color: KIND_COLOR[f.kind],
393
+ background: "var(--dsw-hover)",
394
+ border: "1px solid currentColor",
395
+ }}
396
+ >
397
+ {f.kind === "untracked" ? "?" : f.kind === "conflict" ? "!" : mode === "staged" ? f.indexStatus : f.workStatus !== " " ? f.workStatus : f.indexStatus}
398
+ </span>
399
+ <span className="min-w-0 flex-1 truncate" style={{ fontSize: 12, fontFamily: "var(--font-mono)", color: "var(--dsw-label-secondary)" }}>
400
+ {f.path}
401
+ </span>
402
+ </button>
403
+ ))}
404
+ </div>
405
+ </div>
406
+ ))
407
+ )}
408
+
409
+ {/* 最近提交 */}
410
+ <div className="hairline-t pt-3">
411
+ <div className="mb-1" style={{ fontSize: 11.5, fontWeight: 600, color: "var(--dsw-label-caption)" }}>{t.gitRecentCommits}</div>
412
+ {info.commits.length === 0 ? (
413
+ <div style={{ fontSize: 12.5, color: "var(--dsw-label-caption)", padding: "4px 2px" }}>{t.gitNoCommits}</div>
414
+ ) : (
415
+ <div className="flex flex-col gap-0.5">
416
+ {info.commits.map((c) => (
417
+ <button
418
+ key={c.hash}
419
+ type="button"
420
+ className="flex w-full flex-col items-stretch rounded-xl px-2.5 py-1.5 text-left transition-all"
421
+ style={{ transition: "all 0.18s var(--isx-ease)" }}
422
+ onMouseEnter={(e) => {
423
+ e.currentTarget.style.background = "var(--dsw-hover)";
424
+ e.currentTarget.style.transform = "translateX(2px)";
425
+ }}
426
+ onMouseLeave={(e) => {
427
+ e.currentTarget.style.background = "transparent";
428
+ e.currentTarget.style.transform = "none";
429
+ }}
430
+ onClick={() => changeView({ kind: "commit", hash: c.hash })}
431
+ >
432
+ <span className="flex items-baseline gap-2">
433
+ <span className="flex-none rounded-md px-1.5 py-0.5" style={{ fontSize: 10.5, fontFamily: "var(--font-mono)", fontWeight: 600, color: "var(--dsw-accent)", background: "var(--dsw-accent-soft)" }}>{c.hash}</span>
434
+ <span className="min-w-0 flex-1 truncate" style={{ fontSize: 12, color: "var(--dsw-label-secondary)" }} title={c.subject}>{c.subject}</span>
435
+ </span>
436
+ <span className="truncate" style={{ fontSize: 11, color: "var(--dsw-label-caption)", paddingLeft: 0, marginTop: 2 }}>
437
+ {c.author} · {c.relative}
438
+ </span>
439
+ </button>
440
+ ))}
441
+ </div>
442
+ )}
443
+ </div>
444
+ </div>
445
+ )}
446
+ </div>
447
+ </div>
448
+ );
449
+ }