@rexvane/piweb 0.3.21 → 0.3.22

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 (293) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +6 -4
  3. package/.next/build-manifest.json +5 -5
  4. package/.next/next-minimal-server.js.nft.json +1 -1
  5. package/.next/next-server.js.nft.json +1 -1
  6. package/.next/prerender-manifest.json +7 -7
  7. package/.next/react-loadable-manifest.json +10 -10
  8. package/.next/required-server-files.js +16 -1
  9. package/.next/required-server-files.json +16 -1
  10. package/.next/routes-manifest.json +12 -0
  11. package/.next/server/app/_global-error/page.js +2 -2
  12. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  13. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/_global-error.html +1 -1
  15. package/.next/server/app/_global-error.meta +8 -0
  16. package/.next/server/app/_global-error.rsc +15 -11
  17. package/.next/server/app/_global-error.segments/_full.segment.rsc +15 -11
  18. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +20 -16
  19. package/.next/server/app/_global-error.segments/_tree.segment.rsc +3 -1
  20. package/.next/server/app/_not-found/page.js +1 -1
  21. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  22. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  23. package/.next/server/app/_not-found.html +1 -1
  24. package/.next/server/app/_not-found.meta +8 -0
  25. package/.next/server/app/_not-found.rsc +17 -13
  26. package/.next/server/app/_not-found.segments/_full.segment.rsc +17 -13
  27. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +24 -20
  28. package/.next/server/app/_not-found.segments/_tree.segment.rsc +4 -2
  29. package/.next/server/app/api/agent/[id]/events/route.js +2 -2
  30. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
  31. package/.next/server/app/api/agent/[id]/route.js +1 -1
  32. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
  33. package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
  34. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +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/config/route.js +1 -1
  38. package/.next/server/app/api/config/route.js.nft.json +1 -1
  39. package/.next/server/app/api/files/route.js +1 -1
  40. package/.next/server/app/api/files/route.js.nft.json +1 -1
  41. package/.next/server/app/api/fs/route.js +1 -1
  42. package/.next/server/app/api/fs/route.js.nft.json +1 -1
  43. package/.next/server/app/api/git/route.js +4 -4
  44. package/.next/server/app/api/git/route.js.nft.json +1 -1
  45. package/.next/server/app/api/github/route.js +5 -0
  46. package/.next/server/app/api/github/route.js.nft.json +1 -0
  47. package/.next/server/app/api/github/route_client-reference-manifest.js +1 -0
  48. package/.next/server/app/api/growth/route.js +1 -1
  49. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  50. package/.next/server/app/api/health/route.js +1 -1
  51. package/.next/server/app/api/health/route.js.nft.json +1 -1
  52. package/.next/server/app/api/models/defaults/route.js +1 -0
  53. package/.next/server/app/api/models/defaults/route.js.nft.json +1 -0
  54. package/.next/server/app/api/models/defaults/route_client-reference-manifest.js +1 -0
  55. package/.next/server/app/api/models/route.js +1 -1
  56. package/.next/server/app/api/models/route.js.nft.json +1 -1
  57. package/.next/server/app/api/pi-settings/route.js +1 -1
  58. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  59. package/.next/server/app/api/plugins/route.js +1 -1
  60. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  61. package/.next/server/app/api/prompts/route.js +1 -1
  62. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  63. package/.next/server/app/api/remote/route.js +3 -3
  64. package/.next/server/app/api/remote/route.js.nft.json +1 -1
  65. package/.next/server/app/api/security/route.js +1 -1
  66. package/.next/server/app/api/security/route.js.nft.json +1 -1
  67. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  68. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  69. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  70. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  71. package/.next/server/app/api/sessions/import/route.js +2 -2
  72. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
  73. package/.next/server/app/api/sessions/route.js +1 -1
  74. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  75. package/.next/server/app/api/skills/route.js +2 -2
  76. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  77. package/.next/server/app/api/update/route.js +2 -2
  78. package/.next/server/app/api/update/route.js.nft.json +1 -1
  79. package/.next/server/app/api/version/route.js +1 -1
  80. package/.next/server/app/api/version/route.js.nft.json +1 -1
  81. package/.next/server/app/api/web-auth/route.js +1 -1
  82. package/.next/server/app/api/web-auth/route.js.nft.json +1 -1
  83. package/.next/server/app/api/workspaces/route.js +1 -1
  84. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  85. package/.next/server/app/icon.svg/route.js +1 -1
  86. package/.next/server/app/icon.svg/route.js.nft.json +1 -1
  87. package/.next/server/app/icon.svg.meta +1 -1
  88. package/.next/server/app/index.html +1 -1
  89. package/.next/server/app/index.meta +8 -0
  90. package/.next/server/app/index.rsc +19 -13
  91. package/.next/server/app/index.segments/__PAGE__.segment.rsc +25 -19
  92. package/.next/server/app/index.segments/_full.segment.rsc +19 -13
  93. package/.next/server/app/index.segments/_tree.segment.rsc +3 -2
  94. package/.next/server/app/login/page.js +1 -1
  95. package/.next/server/app/login/page.js.nft.json +1 -1
  96. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  97. package/.next/server/app/login.html +1 -1
  98. package/.next/server/app/login.meta +8 -0
  99. package/.next/server/app/login.rsc +23 -17
  100. package/.next/server/app/login.segments/_full.segment.rsc +23 -17
  101. package/.next/server/app/login.segments/_tree.segment.rsc +4 -2
  102. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +30 -24
  103. package/.next/server/app/page.js +1 -1
  104. package/.next/server/app/page.js.nft.json +1 -1
  105. package/.next/server/app/page_client-reference-manifest.js +1 -1
  106. package/.next/server/app-paths-manifest.json +6 -4
  107. package/.next/server/chunks/229.js +1 -1
  108. package/.next/server/chunks/421.js +1 -1
  109. package/.next/server/chunks/430.js +215 -31
  110. package/.next/server/chunks/496.js +1 -1
  111. package/.next/server/chunks/538.js +1 -1
  112. package/.next/server/chunks/662.js +2 -2
  113. package/.next/server/chunks/686.js +1 -1
  114. package/.next/server/chunks/813.js +23 -1
  115. package/.next/server/chunks/920.js +2 -2
  116. package/.next/server/chunks/924.js +1 -1
  117. package/.next/server/chunks/970.js +305 -0
  118. package/.next/server/middleware-build-manifest.js +1 -1
  119. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  120. package/.next/server/middleware.js +185 -23
  121. package/.next/server/middleware.js.nft.json +1 -1
  122. package/.next/server/pages/404.html +1 -1
  123. package/.next/server/pages/500.html +1 -1
  124. package/.next/server/preview-props.json +5 -0
  125. package/.next/server/server-reference-manifest.json +1 -1
  126. package/.next/static/WMazYvD-gbeLsCUfUxvvM/_buildManifest.js +1 -0
  127. package/.next/static/chunks/316-698d10eb465bd8ef.js +1 -0
  128. package/.next/static/chunks/3794-0978865cda773008.js +52 -0
  129. package/.next/static/chunks/4673.4cf66cbdd8364c57.js +1 -0
  130. package/.next/static/chunks/4686-09fcd0028079006c.js +1 -0
  131. package/.next/static/chunks/4778.eddd3a5813f85824.js +1 -0
  132. package/.next/static/chunks/4bd1b696-e199e788e58acf79.js +1 -0
  133. package/.next/static/chunks/{5173.91ddb803785009f8.js → 5173.dfd1cbb795b9a21c.js} +1 -1
  134. package/.next/static/chunks/618.949981da99be7182.js +4 -0
  135. package/.next/static/chunks/7411.67366dc5032a141d.js +1 -0
  136. package/.next/static/chunks/app/_global-error/page-f977af7f858990a2.js +1 -0
  137. package/.next/static/chunks/app/api/agent/[id]/events/route-f977af7f858990a2.js +1 -0
  138. package/.next/static/chunks/app/api/agent/[id]/route-f977af7f858990a2.js +1 -0
  139. package/.next/static/chunks/app/api/agent/[id]/tree/route-f977af7f858990a2.js +1 -0
  140. package/.next/static/chunks/app/api/agent/new/route-f977af7f858990a2.js +1 -0
  141. package/.next/static/chunks/app/api/config/route-f977af7f858990a2.js +1 -0
  142. package/.next/static/chunks/app/api/files/route-f977af7f858990a2.js +1 -0
  143. package/.next/static/chunks/app/api/fs/route-f977af7f858990a2.js +1 -0
  144. package/.next/static/chunks/app/api/git/route-f977af7f858990a2.js +1 -0
  145. package/.next/static/chunks/app/api/github/route-f977af7f858990a2.js +1 -0
  146. package/.next/static/chunks/app/api/growth/route-f977af7f858990a2.js +1 -0
  147. package/.next/static/chunks/app/api/health/route-f977af7f858990a2.js +1 -0
  148. package/.next/static/chunks/app/api/models/defaults/route-f977af7f858990a2.js +1 -0
  149. package/.next/static/chunks/app/api/models/route-f977af7f858990a2.js +1 -0
  150. package/.next/static/chunks/app/api/pi-settings/route-f977af7f858990a2.js +1 -0
  151. package/.next/static/chunks/app/api/plugins/route-f977af7f858990a2.js +1 -0
  152. package/.next/static/chunks/app/api/prompts/route-f977af7f858990a2.js +1 -0
  153. package/.next/static/chunks/app/api/remote/route-f977af7f858990a2.js +1 -0
  154. package/.next/static/chunks/app/api/security/route-f977af7f858990a2.js +1 -0
  155. package/.next/static/chunks/app/api/sessions/[id]/export/route-f977af7f858990a2.js +1 -0
  156. package/.next/static/chunks/app/api/sessions/[id]/route-f977af7f858990a2.js +1 -0
  157. package/.next/static/chunks/app/api/sessions/import/route-f977af7f858990a2.js +1 -0
  158. package/.next/static/chunks/app/api/sessions/route-f977af7f858990a2.js +1 -0
  159. package/.next/static/chunks/app/api/skills/route-f977af7f858990a2.js +1 -0
  160. package/.next/static/chunks/app/api/update/route-f977af7f858990a2.js +1 -0
  161. package/.next/static/chunks/app/api/version/route-f977af7f858990a2.js +1 -0
  162. package/.next/static/chunks/app/api/web-auth/route-f977af7f858990a2.js +1 -0
  163. package/.next/static/chunks/app/api/workspaces/route-f977af7f858990a2.js +1 -0
  164. package/.next/static/chunks/app/page-fc4ed2ff29b6235e.js +1 -0
  165. package/.next/static/chunks/framework-47b723d27f4a4963.js +1 -0
  166. package/.next/static/chunks/main-5403f0764579e5fe.js +5 -0
  167. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f977af7f858990a2.js +1 -0
  168. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f977af7f858990a2.js +1 -0
  169. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f977af7f858990a2.js +1 -0
  170. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f977af7f858990a2.js +1 -0
  171. package/.next/static/chunks/webpack-22ebffc20adb44ae.js +1 -0
  172. package/.next/static/css/6671d0d1589fdc0e.css +1 -0
  173. package/.next/static/css/{2959bd6b626b7ffe.css → f15a853af8b5127f.css} +1 -1
  174. package/.next/types/app/api/agent/[id]/events/route.ts +3 -1
  175. package/.next/types/app/api/agent/[id]/route.ts +3 -1
  176. package/.next/types/app/api/agent/[id]/tree/route.ts +3 -1
  177. package/.next/types/app/api/agent/new/route.ts +3 -1
  178. package/.next/types/app/api/config/route.ts +3 -1
  179. package/.next/types/app/api/files/route.ts +3 -1
  180. package/.next/types/app/api/fs/route.ts +3 -1
  181. package/.next/types/app/api/git/route.ts +3 -1
  182. package/.next/types/app/api/github/route.ts +355 -0
  183. package/.next/types/app/api/growth/route.ts +3 -1
  184. package/.next/types/app/api/health/route.ts +3 -1
  185. package/.next/types/app/api/models/defaults/route.ts +355 -0
  186. package/.next/types/app/api/models/route.ts +3 -1
  187. package/.next/types/app/api/pi-settings/route.ts +3 -1
  188. package/.next/types/app/api/plugins/route.ts +3 -1
  189. package/.next/types/app/api/prompts/route.ts +3 -1
  190. package/.next/types/app/api/remote/route.ts +3 -1
  191. package/.next/types/app/api/security/route.ts +3 -1
  192. package/.next/types/app/api/sessions/[id]/export/route.ts +3 -1
  193. package/.next/types/app/api/sessions/[id]/route.ts +3 -1
  194. package/.next/types/app/api/sessions/import/route.ts +3 -1
  195. package/.next/types/app/api/sessions/route.ts +3 -1
  196. package/.next/types/app/api/skills/route.ts +3 -1
  197. package/.next/types/app/api/update/route.ts +3 -1
  198. package/.next/types/app/api/version/route.ts +3 -1
  199. package/.next/types/app/api/web-auth/route.ts +3 -1
  200. package/.next/types/app/api/workspaces/route.ts +3 -1
  201. package/.next/types/app/layout.ts +4 -1
  202. package/.next/types/app/login/page.ts +4 -1
  203. package/.next/types/app/page.ts +4 -1
  204. package/.next/types/cache-life.d.ts +11 -43
  205. package/.next/types/routes.d.ts +3 -1
  206. package/.next/types/validator.ts +76 -2
  207. package/package.json +2 -2
  208. package/src/app/api/files/route.ts +5 -2
  209. package/src/app/api/github/route.ts +26 -0
  210. package/src/app/api/models/defaults/route.ts +18 -0
  211. package/src/app/api/models/route.ts +24 -1
  212. package/src/app/globals.css +186 -0
  213. package/src/components/AppShell.tsx +82 -30
  214. package/src/components/ChatInput.tsx +244 -63
  215. package/src/components/ChatWindow.tsx +18 -27
  216. package/src/components/ContextMeter.tsx +193 -143
  217. package/src/components/GitPanel.tsx +110 -40
  218. package/src/components/ProjectPanel.tsx +55 -30
  219. package/src/components/PromptPanel.tsx +2 -4
  220. package/src/components/ProviderSetupModal.module.css +20 -0
  221. package/src/components/ProviderSetupModal.tsx +134 -29
  222. package/src/components/SettingsPanel.tsx +243 -27
  223. package/src/components/TrajectoryView.tsx +4 -6
  224. package/src/components/composer/ComposerImages.tsx +306 -0
  225. package/src/hooks/useGrowth.ts +3 -0
  226. package/src/hooks/usePiWeb.ts +12 -2
  227. package/src/hooks/useWorkspaceFiles.ts +93 -0
  228. package/src/i18n.tsx +108 -2
  229. package/src/lib/agent-manager.ts +135 -104
  230. package/src/lib/files-service.ts +100 -1
  231. package/src/lib/format.ts +56 -0
  232. package/src/lib/git-service.ts +45 -13
  233. package/src/lib/github-identity.ts +245 -0
  234. package/src/lib/growth-service.ts +6 -2
  235. package/src/lib/hero-model-memory.ts +28 -0
  236. package/src/lib/model-defaults.ts +105 -0
  237. package/src/lib/model-draft.ts +257 -9
  238. package/src/lib/model-endpoint.ts +23 -0
  239. package/src/lib/model-files.ts +33 -0
  240. package/src/lib/models-service.ts +240 -78
  241. package/src/lib/pi.ts +27 -2
  242. package/src/lib/process-format.ts +3 -2
  243. package/src/lib/session/metrics.ts +31 -0
  244. package/src/lib/thinking-level.ts +39 -0
  245. package/src/lib/trajectory.ts +17 -3
  246. package/src/lib/types.ts +12 -1
  247. package/src/lib/workspace-index.ts +270 -0
  248. package/.next/server/chunks/741.js +0 -165
  249. package/.next/static/chunks/316-5ea443ee6b512aa9.js +0 -1
  250. package/.next/static/chunks/3794-3679f1c05160977c.js +0 -52
  251. package/.next/static/chunks/4673.8a5a9e6855350e34.js +0 -1
  252. package/.next/static/chunks/4686-882531aef1d2e250.js +0 -1
  253. package/.next/static/chunks/4778.305dc6abb9889627.js +0 -1
  254. package/.next/static/chunks/4bd1b696-93bf506b236d6248.js +0 -1
  255. package/.next/static/chunks/618.ee5d5bf1c647e483.js +0 -4
  256. package/.next/static/chunks/839.bfd2db36483faa3e.js +0 -1
  257. package/.next/static/chunks/app/_global-error/page-b179d9f7f6f2daf9.js +0 -1
  258. package/.next/static/chunks/app/api/agent/[id]/events/route-b179d9f7f6f2daf9.js +0 -1
  259. package/.next/static/chunks/app/api/agent/[id]/route-b179d9f7f6f2daf9.js +0 -1
  260. package/.next/static/chunks/app/api/agent/[id]/tree/route-b179d9f7f6f2daf9.js +0 -1
  261. package/.next/static/chunks/app/api/agent/new/route-b179d9f7f6f2daf9.js +0 -1
  262. package/.next/static/chunks/app/api/config/route-b179d9f7f6f2daf9.js +0 -1
  263. package/.next/static/chunks/app/api/files/route-b179d9f7f6f2daf9.js +0 -1
  264. package/.next/static/chunks/app/api/fs/route-b179d9f7f6f2daf9.js +0 -1
  265. package/.next/static/chunks/app/api/git/route-b179d9f7f6f2daf9.js +0 -1
  266. package/.next/static/chunks/app/api/growth/route-b179d9f7f6f2daf9.js +0 -1
  267. package/.next/static/chunks/app/api/health/route-b179d9f7f6f2daf9.js +0 -1
  268. package/.next/static/chunks/app/api/models/route-b179d9f7f6f2daf9.js +0 -1
  269. package/.next/static/chunks/app/api/pi-settings/route-b179d9f7f6f2daf9.js +0 -1
  270. package/.next/static/chunks/app/api/plugins/route-b179d9f7f6f2daf9.js +0 -1
  271. package/.next/static/chunks/app/api/prompts/route-b179d9f7f6f2daf9.js +0 -1
  272. package/.next/static/chunks/app/api/remote/route-b179d9f7f6f2daf9.js +0 -1
  273. package/.next/static/chunks/app/api/security/route-b179d9f7f6f2daf9.js +0 -1
  274. package/.next/static/chunks/app/api/sessions/[id]/export/route-b179d9f7f6f2daf9.js +0 -1
  275. package/.next/static/chunks/app/api/sessions/[id]/route-b179d9f7f6f2daf9.js +0 -1
  276. package/.next/static/chunks/app/api/sessions/import/route-b179d9f7f6f2daf9.js +0 -1
  277. package/.next/static/chunks/app/api/sessions/route-b179d9f7f6f2daf9.js +0 -1
  278. package/.next/static/chunks/app/api/skills/route-b179d9f7f6f2daf9.js +0 -1
  279. package/.next/static/chunks/app/api/update/route-b179d9f7f6f2daf9.js +0 -1
  280. package/.next/static/chunks/app/api/version/route-b179d9f7f6f2daf9.js +0 -1
  281. package/.next/static/chunks/app/api/web-auth/route-b179d9f7f6f2daf9.js +0 -1
  282. package/.next/static/chunks/app/api/workspaces/route-b179d9f7f6f2daf9.js +0 -1
  283. package/.next/static/chunks/app/page-c04af72e549695bd.js +0 -1
  284. package/.next/static/chunks/framework-c4e651fe89edfa91.js +0 -1
  285. package/.next/static/chunks/main-6159b22a8cb0b7c5.js +0 -5
  286. package/.next/static/chunks/next/dist/client/components/builtin/app-error-b179d9f7f6f2daf9.js +0 -1
  287. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-b179d9f7f6f2daf9.js +0 -1
  288. package/.next/static/chunks/next/dist/client/components/builtin/not-found-b179d9f7f6f2daf9.js +0 -1
  289. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-b179d9f7f6f2daf9.js +0 -1
  290. package/.next/static/chunks/webpack-9b4bb574e5817654.js +0 -1
  291. package/.next/static/css/ea27b9e30f76303f.css +0 -1
  292. package/.next/static/sNX5GQkOWuK5YEID1hT-C/_buildManifest.js +0 -1
  293. /package/.next/static/{sNX5GQkOWuK5YEID1hT-C → WMazYvD-gbeLsCUfUxvvM}/_ssgManifest.js +0 -0
@@ -0,0 +1,306 @@
1
+ "use client";
2
+
3
+ /**
4
+ * 输入卡里待发送的图片。
5
+ * - 不超过 COLLAPSE_AT 张:平铺缩略图(不裁切),右上角删除。
6
+ * - 更多时折叠成牌堆:正面那张在最上,后面最多露 4 层 4px 的右边缘;点「展开 N」沿一条向上拱的弧线扇开。
7
+ * 两种状态是同一批卡片换 transform,所以有从牌堆扇开 / 收拢的动画;扇形按容器实测宽度铺开。
8
+ * 层级始终是左压右(和牌堆同一个顺序,展开时不会闪一下换层),每张露出来的是右侧那条,
9
+ * 删除按钮就在那条的右上角——不用悬停也点得到,触屏同样能删;张数多、条带窄到按钮会挤成一排时,
10
+ * 只在悬停 / 聚焦的那张上显示(触屏点开预览里删)。悬停 / 聚焦的那张抬起来、整张露出。
11
+ * 牌堆里后面几张只露空白的卡边(不露图),看起来是一叠;扇开时图再淡入。
12
+ * - 点任意一张进预览:大图、← → 切换、删除当前张(删完自动看下一张);Esc / 点背景 / 关闭按钮退出,焦点回到原处。
13
+ * 预览挂到 body 上(祖先的 backdrop-filter 会让 fixed 只盖住输入卡),按键在捕获阶段拦下,不会再关掉页面上的其它面板。
14
+ */
15
+ import { useEffect, useRef, useState } from "react";
16
+ import { createPortal } from "react-dom";
17
+ import { useI18n } from "@/i18n";
18
+ import type { ImageAttachment } from "@/lib/types";
19
+
20
+ /** 超过这个数量就折叠(引用 chip 用同一个阈值) */
21
+ export const COLLAPSE_AT = 4;
22
+ /** 牌堆 / 扇形里卡片的边长 */
23
+ const CARD = 96;
24
+ /** 牌堆里后面每层露出的右边缘、最多露几层(再多的叠在最后一层下面、不显示) */
25
+ const DECK_PEEK = 4;
26
+ const DECK_LAYERS = 4;
27
+ /** 扇形所在圆弧的半径(越大弧越平)、相邻两张中心的最大间距(再大就散成一排,不像一手牌了) */
28
+ const FAN_RADIUS = 520;
29
+ const FAN_GAP_MAX = 76;
30
+ /** 悬停 / 聚焦时那张往上抬多少;左右给旋转后探出去的角留的余量 */
31
+ const FAN_LIFT = 12;
32
+ const FAN_SIDE = 8;
33
+ /** 量不到宽度时(首帧、测试环境)按这个宽度排扇形 */
34
+ const FALLBACK_WIDTH = 360;
35
+
36
+ export interface CardPose {
37
+ x: number;
38
+ y: number;
39
+ angle: number;
40
+ /** 牌堆里压在最后一层下面、看不见的 */
41
+ hidden?: boolean;
42
+ }
43
+
44
+ interface Layout {
45
+ cards: CardPose[];
46
+ width: number;
47
+ height: number;
48
+ /** 相邻两张露出来的宽度;太窄时删除按钮会挤成一排,只在悬停 / 聚焦那张上显示 */
49
+ gap: number;
50
+ }
51
+
52
+ /** 露出的条带至少这么宽,每张的删除按钮才常驻(按钮 22px + 一点间隔) */
53
+ const ROOMY_GAP = 30;
54
+
55
+ const round = (value: number) => Math.round(value * 10) / 10;
56
+
57
+ /** 牌堆:只做横向错位,最多露 DECK_LAYERS 层 */
58
+ export function deckLayout(count: number): Layout {
59
+ const cards = Array.from({ length: count }, (_, i) => ({ x: Math.min(i, DECK_LAYERS) * DECK_PEEK, y: 0, angle: 0, hidden: i > DECK_LAYERS }));
60
+ return { cards, width: CARD + Math.min(Math.max(count - 1, 0), DECK_LAYERS) * DECK_PEEK, height: CARD, gap: DECK_PEEK };
61
+ }
62
+
63
+ /**
64
+ * 扇形:卡片中心落在一段圆弧上(中间最高、两侧顺着弧下垂),每张转到切线方向;
65
+ * 间距按可用宽度均分、不超过 FAN_GAP_MAX,从左边开始铺(离牌堆原来的位置最近)。
66
+ */
67
+ export function fanLayout(count: number, width: number): Layout {
68
+ const gap = count > 1 ? Math.min(FAN_GAP_MAX, Math.max(8, (width - CARD - FAN_SIDE * 2) / (count - 1))) : 0;
69
+ const step = gap / FAN_RADIUS;
70
+ const mid = (count - 1) / 2;
71
+ const edge = -mid * step;
72
+ // 正方形转过 θ 后比原来高出(也宽出)的那一截,最外侧两张最大
73
+ const corner = (CARD / 2) * (Math.abs(Math.sin(edge)) + Math.cos(edge) - 1);
74
+ const top = FAN_LIFT + corner;
75
+ const cards = Array.from({ length: count }, (_, i) => {
76
+ const theta = (i - mid) * step;
77
+ return {
78
+ x: round(FAN_SIDE + FAN_RADIUS * (Math.sin(theta) - Math.sin(edge))),
79
+ y: round(top + FAN_RADIUS * (1 - Math.cos(theta))),
80
+ angle: round((theta * 180) / Math.PI),
81
+ };
82
+ });
83
+ const span = cards.length ? cards[cards.length - 1].x + CARD + FAN_SIDE : 0;
84
+ return { cards, width: Math.ceil(span), height: Math.ceil(top + FAN_RADIUS * (1 - Math.cos(edge)) + CARD + corner), gap: round(gap) };
85
+ }
86
+
87
+ /** 每张图一个稳定的 key:前几位 base64 对同格式的图几乎都一样(PNG 都是 iVBORw0KGgo…),不能拿来当 key */
88
+ const imageKeys = new WeakMap<ImageAttachment, string>();
89
+ let nextImageKey = 0;
90
+ function keyOf(image: ImageAttachment): string {
91
+ let key = imageKeys.get(image);
92
+ if (!key) {
93
+ nextImageKey += 1;
94
+ key = `image-${nextImageKey}`;
95
+ imageKeys.set(image, key);
96
+ }
97
+ return key;
98
+ }
99
+
100
+ const srcOf = (image: ImageAttachment) => `data:${image.mimeType};base64,${image.data}`;
101
+
102
+ export function ComposerImages({ images, disabled, onRemove }: { images: ImageAttachment[]; disabled?: boolean; onRemove: (index: number) => void }) {
103
+ const { t } = useI18n();
104
+ const collapsible = images.length > COLLAPSE_AT;
105
+ const [expanded, setExpanded] = useState(false);
106
+ const fanned = collapsible && expanded;
107
+ /** 扇形里悬停 / 聚焦的那张:抬起来、放到最上层 */
108
+ const [lifted, setLifted] = useState<number | null>(null);
109
+ const [preview, setPreview] = useState<number | null>(null);
110
+ const railRef = useRef<HTMLDivElement>(null);
111
+ const [railWidth, setRailWidth] = useState(0);
112
+
113
+ useEffect(() => {
114
+ const rail = railRef.current;
115
+ if (!rail || !collapsible) return;
116
+ // 量的是整条附件轨的内容宽度(去掉左右 4px 内边距):扇形展开后占满这一行
117
+ const measure = () => setRailWidth(Math.max(0, rail.clientWidth - 8));
118
+ measure();
119
+ if (typeof ResizeObserver === "undefined") return;
120
+ const observer = new ResizeObserver(measure);
121
+ observer.observe(rail);
122
+ return () => observer.disconnect();
123
+ }, [collapsible]);
124
+
125
+ const remove = (index: number) => {
126
+ onRemove(index);
127
+ setLifted(null);
128
+ // 预览里删的:还有图就接着看下一张(删的是最后一张就看前一张)
129
+ setPreview((current) => (current === null ? null : images.length <= 1 ? null : Math.min(current, images.length - 2)));
130
+ };
131
+
132
+ const deleteButton = (index: number) => (
133
+ <button type="button" className="pw-image-delete" disabled={disabled} aria-label={`${t.delete} ${index + 1}`} onClick={() => remove(index)}>
134
+ ✕
135
+ </button>
136
+ );
137
+
138
+ const layout = fanned ? fanLayout(images.length, railWidth || FALLBACK_WIDTH) : deckLayout(images.length);
139
+ const shown = preview !== null && images[preview] ? preview : null;
140
+
141
+ return (
142
+ <>
143
+ <div ref={railRef} className="mb-2 flex flex-wrap items-center gap-2 px-1" data-testid="composer-images">
144
+ {collapsible ? (
145
+ <div
146
+ className="pw-image-fan"
147
+ data-testid="image-fan"
148
+ data-expanded={fanned}
149
+ data-crowded={fanned && layout.gap < ROOMY_GAP ? true : undefined}
150
+ style={{ width: fanned ? "100%" : layout.width, height: layout.height }}
151
+ >
152
+ {images.map((image, i) => {
153
+ const pose = layout.cards[i];
154
+ const up = fanned && lifted === i;
155
+ // 牌堆里只有正面那张能点、能删;后面那几张只是露个边,不进 Tab 顺序
156
+ const reachable = fanned || i === 0;
157
+ // 展开从左往右依次扇出,收起反过来;错开的总时长封顶,张数多也不拖沓
158
+ const delay = Math.min(fanned ? i * 14 : (images.length - 1 - i) * 10, 140);
159
+ return (
160
+ <div
161
+ key={keyOf(image)}
162
+ className="pw-image-card"
163
+ data-hidden={pose.hidden || undefined}
164
+ data-lifted={up || undefined}
165
+ style={{
166
+ zIndex: up ? images.length + 1 : images.length - i,
167
+ transform: `translate(${pose.x}px, ${pose.y - (up ? FAN_LIFT : 0)}px) rotate(${pose.angle}deg)`,
168
+ transitionDelay: up || lifted !== null ? "0ms" : `${delay}ms`,
169
+ }}
170
+ onMouseEnter={() => fanned && setLifted(i)}
171
+ onMouseLeave={() => setLifted((current) => (current === i ? null : current))}
172
+ onFocus={() => fanned && setLifted(i)}
173
+ onBlur={(event) => {
174
+ // 焦点从图片挪到同一张的删除按钮不算离开
175
+ if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setLifted((current) => (current === i ? null : current));
176
+ }}
177
+ >
178
+ <button
179
+ type="button"
180
+ className="pw-image-face"
181
+ tabIndex={reachable ? undefined : -1}
182
+ aria-hidden={reachable ? undefined : true}
183
+ title={t.imagePreview}
184
+ aria-label={t.imagePreview}
185
+ onClick={() => setPreview(i)}
186
+ >
187
+ <img src={srcOf(image)} alt="" className="h-full w-full object-contain" draggable={false} />
188
+ </button>
189
+ {reachable ? deleteButton(i) : null}
190
+ </div>
191
+ );
192
+ })}
193
+ </div>
194
+ ) : (
195
+ images.map((image, i) => (
196
+ <div key={keyOf(image)} className="pw-image-thumb">
197
+ <button type="button" className="block" title={t.imagePreview} aria-label={t.imagePreview} onClick={() => setPreview(i)}>
198
+ <img src={srcOf(image)} alt="" className="max-h-24 max-w-32 rounded-[10px] object-contain" draggable={false} />
199
+ </button>
200
+ {deleteButton(i)}
201
+ </div>
202
+ ))
203
+ )}
204
+ {collapsible ? (
205
+ <button
206
+ type="button"
207
+ className="pw-chip"
208
+ data-testid="images-toggle"
209
+ aria-expanded={fanned}
210
+ onClick={() => {
211
+ setExpanded((open) => !open);
212
+ setLifted(null);
213
+ }}
214
+ >
215
+ {fanned ? t.chipCollapse : t.chipExpandMore.replace("{n}", String(images.length - 1))}
216
+ </button>
217
+ ) : null}
218
+ </div>
219
+ {shown !== null
220
+ ? createPortal(
221
+ <ImageViewer images={images} index={shown} disabled={disabled} onIndex={setPreview} onRemove={remove} onClose={() => setPreview(null)} />,
222
+ document.body,
223
+ )
224
+ : null}
225
+ </>
226
+ );
227
+ }
228
+
229
+ /** 放大预览:手机上没法用本机程序打开图片,应用内看最实在 */
230
+ function ImageViewer({
231
+ images,
232
+ index,
233
+ disabled,
234
+ onIndex,
235
+ onRemove,
236
+ onClose,
237
+ }: {
238
+ images: ImageAttachment[];
239
+ index: number;
240
+ disabled?: boolean;
241
+ onIndex: (index: number) => void;
242
+ onRemove: (index: number) => void;
243
+ onClose: () => void;
244
+ }) {
245
+ const { t } = useI18n();
246
+ const dialogRef = useRef<HTMLDivElement>(null);
247
+ const closeRef = useRef<HTMLButtonElement>(null);
248
+ const count = images.length;
249
+ const go = (delta: number) => onIndex((index + delta + count) % count);
250
+
251
+ // 打开时把焦点移进来,关掉时还给原来那个元素(还在页面上的话)
252
+ useEffect(() => {
253
+ const opener = document.activeElement instanceof HTMLElement ? document.activeElement : null;
254
+ closeRef.current?.focus();
255
+ return () => {
256
+ if (opener?.isConnected) opener.focus();
257
+ };
258
+ }, []);
259
+
260
+ // 捕获阶段处理:Esc / 方向键不再传给页面上其它监听 document 的面板;Tab 只在预览里转
261
+ useEffect(() => {
262
+ const onKey = (event: KeyboardEvent) => {
263
+ if (event.key === "Escape") onClose();
264
+ else if (event.key === "ArrowLeft" && count > 1) go(-1);
265
+ else if (event.key === "ArrowRight" && count > 1) go(1);
266
+ else if (event.key === "Tab") {
267
+ const focusable = [...(dialogRef.current?.querySelectorAll<HTMLElement>("button:not(:disabled)") ?? [])];
268
+ if (!focusable.length) return;
269
+ const at = focusable.indexOf(document.activeElement as HTMLElement);
270
+ const next = event.shiftKey ? (at <= 0 ? focusable.length - 1 : at - 1) : at === focusable.length - 1 ? 0 : at + 1;
271
+ focusable[next].focus();
272
+ } else return;
273
+ event.preventDefault();
274
+ event.stopPropagation();
275
+ };
276
+ document.addEventListener("keydown", onKey, true);
277
+ return () => document.removeEventListener("keydown", onKey, true);
278
+ // onClose / onIndex 来自父组件的 setState 包装,语义不变;只随张数和当前张重绑
279
+ // eslint-disable-next-line react-hooks/exhaustive-deps
280
+ }, [count, index]);
281
+
282
+ return (
283
+ <div ref={dialogRef} className="pw-viewer" role="dialog" aria-modal="true" aria-label={t.imageViewer} onClick={onClose}>
284
+ <div className="pw-viewer-bar" onClick={(event) => event.stopPropagation()}>
285
+ <span className="pw-viewer-count">{count > 1 ? `${index + 1} / ${count}` : ""}</span>
286
+ <button type="button" className="pw-viewer-btn" disabled={disabled} onClick={() => onRemove(index)}>
287
+ {t.delete}
288
+ </button>
289
+ <button ref={closeRef} type="button" className="pw-viewer-btn" aria-label={t.close} onClick={onClose}>
290
+ ✕
291
+ </button>
292
+ </div>
293
+ <img src={srcOf(images[index])} alt="" className="pw-viewer-image" onClick={(event) => event.stopPropagation()} />
294
+ {count > 1 ? (
295
+ <>
296
+ <button type="button" className="pw-viewer-nav" data-side="prev" aria-label={t.imagePrev} onClick={(event) => { event.stopPropagation(); go(-1); }}>
297
+ ‹
298
+ </button>
299
+ <button type="button" className="pw-viewer-nav" data-side="next" aria-label={t.imageNext} onClick={(event) => { event.stopPropagation(); go(1); }}>
300
+ ›
301
+ </button>
302
+ </>
303
+ ) : null}
304
+ </div>
305
+ );
306
+ }
@@ -39,6 +39,8 @@ export interface GrowthApi {
39
39
  range: { from: string; to: string } | null;
40
40
  /** 所选范围的变更 */
41
41
  changes: GrowthChange[];
42
+ /** 当前范围的完整 diff 已读取;加载或失败时不能将 [] 当成零改动。 */
43
+ changesReady?: boolean;
42
44
  tree: TreeNode;
43
45
  /** 所选轮的全部文件路径(快速打开用) */
44
46
  filePaths: string[];
@@ -618,6 +620,7 @@ export function useGrowth({
618
620
  scope,
619
621
  range,
620
622
  changes,
623
+ changesReady: inlineChanges !== null || Boolean(range && rangeChanges?.key === `${key}|${range.from}..${range.to}` && !rangeChanges.error),
621
624
  tree,
622
625
  filePaths,
623
626
  expanded,
@@ -76,6 +76,10 @@ export const foldPiWebEvent = (state: PiWebState, evt: WebEvent): PiWebState =>
76
76
  const s = { ...state };
77
77
  switch (evt.type) {
78
78
  case "delta": {
79
+ // 又开始出 token 了:不管有没有收到重试结束事件,重试行都收掉。
80
+ // 只靠 retry_end 不够——SDK 换了事件名、某条路径没发结束事件、旧服务端压根不发,
81
+ // 界面就会挂着「重试 1/5」而模型其实早在正常输出了(用户已反馈两次)。
82
+ s.retryNotice = null;
79
83
  // 新服务端按稳定流 ID 定位;旧协议仍按最近 pending 消息兼容。
80
84
  const msgs = [...s.messages];
81
85
  // 从尾部找:增量几乎总是落在最后一条消息上,长会话里从头扫每个 token 都是一次全表遍历
@@ -251,11 +255,17 @@ export const foldPiWebEvent = (state: PiWebState, evt: WebEvent): PiWebState =>
251
255
  s.growth = { ...s.growth, consent: evt.request };
252
256
  return s;
253
257
  case "retry":
254
- // 只保留最后一次尝试:重试过程在界面上是「重试 3/5」这一条,而不是五行报错
258
+ // 只保留最后一次尝试:重试过程在界面上是「重试 3/5」这一条,而不是五行报错
255
259
  s.retryNotice = evt.maxAttempts
256
260
  ? `重试 ${evt.attempt}/${evt.maxAttempts}${evt.message ? `:${evt.message}` : ""}`
257
261
  : `重试 ${evt.attempt}${evt.message ? `:${evt.message}` : ""}`;
258
262
  return s;
263
+ case "retry_end":
264
+ // 重试区间结束(SDK auto_retry_end):立刻收掉重试行。
265
+ // 重试常发生在下一轮 agent 开始之前,而那段间隙不会发 status:isStreaming=false,
266
+ // 所以不能只等 agent_settled —— 否则用户会一直看到「重试 1/5 terminated」。
267
+ s.retryNotice = null;
268
+ return s;
259
269
  case "error":
260
270
  // 旧服务端把自动重试塞在 error 里:同样按通知处理,不占错误条
261
271
  if (/^(自动重试|重试)/.test(evt.message)) s.retryNotice = evt.message;
@@ -809,7 +819,7 @@ export function usePiWeb() {
809
819
  const id = idOf(p);
810
820
  // 工具预设取通用设置的持久化选择;模型/思考级别由 Hero 页显式下发
811
821
  const setup: Record<string, unknown>[] = [{ cmd: "setToolPreset", preset: savedToolPreset() }];
812
- if (overrides.provider && overrides.modelId) setup.push({ cmd: "setModel", provider: overrides.provider, modelId: overrides.modelId });
822
+ if (overrides.provider && overrides.modelId) setup.push({ cmd: "setModel", provider: overrides.provider, modelId: overrides.modelId, fresh: true });
813
823
  if (overrides.thinking) setup.push({ cmd: "setThinkingLevel", level: overrides.thinking });
814
824
  for (const command of setup) {
815
825
  const configured = await sendCommand(command, id);
@@ -0,0 +1,93 @@
1
+ "use client";
2
+
3
+ /**
4
+ * @ 引用的候选文件:打开候选菜单时拉一次全量清单,之后每个字符都在本地匹配。
5
+ * 按工作区缓存(含 in-flight 去重),过期后**先用手上的旧清单渲染、再后台刷新**——
6
+ * 打字过程中绝不能因为一次刷新把候选列表清空。
7
+ */
8
+ import { useEffect, useRef, useState } from "react";
9
+ import { buildCandidates, dirsOfPaths, type PathCandidate } from "@/lib/workspace-index";
10
+
11
+ export interface WorkspaceFiles {
12
+ status: "idle" | "loading" | "ready" | "error";
13
+ candidates: PathCandidate[];
14
+ source: "git" | "disk" | null;
15
+ truncated: boolean;
16
+ error: string | null;
17
+ }
18
+
19
+ /** 清单多久算过期:agent 刚写出来的新文件也要能被引用到,所以不能一直用内存里那份 */
20
+ const TTL_MS = 30_000;
21
+
22
+ interface CachedIndex {
23
+ at: number;
24
+ candidates: PathCandidate[];
25
+ source: "git" | "disk";
26
+ truncated: boolean;
27
+ }
28
+
29
+ const cache = new Map<string, CachedIndex>();
30
+ const inflight = new Map<string, Promise<CachedIndex>>();
31
+
32
+ async function fetchIndex(cwd: string): Promise<CachedIndex> {
33
+ const response = await fetch(`/api/files?index=1&cwd=${encodeURIComponent(cwd)}`, { cache: "no-store" });
34
+ const payload = await response.json();
35
+ if (!response.ok || !payload.success) throw new Error(payload.error || `request failed (${response.status})`);
36
+ const data = payload.data as { files: string[]; source: "git" | "disk"; truncated: boolean };
37
+ const entry: CachedIndex = {
38
+ at: Date.now(),
39
+ candidates: buildCandidates(data.files, dirsOfPaths(data.files)),
40
+ source: data.source,
41
+ truncated: data.truncated,
42
+ };
43
+ cache.set(cwd, entry);
44
+ return entry;
45
+ }
46
+
47
+ /**
48
+ * 同一工作区并发只发一次请求;失败不进缓存(下次打开会重试)。
49
+ * 共享的请求不挂任何一方的 AbortSignal:先打开的输入框关了(中止),后来的还在等同一个结果,
50
+ * 不能让它跟着收到 AbortError。各方用完就不理结果,请求本身让它跑完、顺手填进缓存。
51
+ */
52
+ function loadIndex(cwd: string): Promise<CachedIndex> {
53
+ const running = inflight.get(cwd);
54
+ if (running) return running;
55
+ const promise = fetchIndex(cwd).finally(() => inflight.delete(cwd));
56
+ inflight.set(cwd, promise);
57
+ return promise;
58
+ }
59
+
60
+ export function useWorkspaceFiles(cwd: string, enabled: boolean): WorkspaceFiles {
61
+ const [state, setState] = useState<WorkspaceFiles>({ status: "idle", candidates: [], source: null, truncated: false, error: null });
62
+ const scope = useRef<{ cwd: string; controller: AbortController }>({ cwd: "", controller: new AbortController() });
63
+
64
+ useEffect(() => {
65
+ if (!enabled || !cwd) return;
66
+ const controller = new AbortController();
67
+ scope.current = { cwd, controller };
68
+ const isCurrent = () => scope.current.controller === controller && !controller.signal.aborted;
69
+
70
+ const cached = cache.get(cwd);
71
+ if (cached) {
72
+ // 有旧清单就先显示(旧的总比空白好),过期只在后台补
73
+ setState({ status: "ready", candidates: cached.candidates, source: cached.source, truncated: cached.truncated, error: null });
74
+ if (Date.now() - cached.at < TTL_MS) return () => controller.abort();
75
+ } else {
76
+ setState({ status: "loading", candidates: [], source: null, truncated: false, error: null });
77
+ }
78
+
79
+ void loadIndex(cwd).then(
80
+ (entry) => {
81
+ if (isCurrent()) setState({ status: "ready", candidates: entry.candidates, source: entry.source, truncated: entry.truncated, error: null });
82
+ },
83
+ (error: unknown) => {
84
+ // 有旧清单时忽略这次失败:刷新失败不该把可用候选变成错误页
85
+ if (!isCurrent() || cache.has(cwd)) return;
86
+ setState({ status: "error", candidates: [], source: null, truncated: false, error: error instanceof Error ? error.message : String(error) });
87
+ },
88
+ );
89
+ return () => controller.abort();
90
+ }, [cwd, enabled]);
91
+
92
+ return state;
93
+ }
package/src/i18n.tsx CHANGED
@@ -38,13 +38,37 @@ const zh = {
38
38
  capacityFields: "容量",
39
39
  contextWindowLabel: "上下文窗口",
40
40
  maxTokensLabel: "最大输出",
41
- capacityHint: "留空用默认 · 支持 256K / 2M 写法",
41
+ capacityHint: "留空按 pi 缺省(128K / 16K)· 支持 256K / 2M 写法",
42
+ // 模型能力(视觉)与配置体检
43
+ visionLabel: "视觉",
44
+ visionHint: "该模型能否接收图片。未声明时 pi 按纯文本处理:你贴的截图会被换成一行占位文字(提示语还会说成「模型不支持图像」)",
45
+ visionUnset: "未声明",
46
+ visionOn: "有视觉",
47
+ visionOff: "纯文本",
48
+ imagePreview: "点击放大预览",
49
+ imageViewer: "图片预览",
50
+ imagePrev: "上一张",
51
+ imageNext: "下一张",
52
+ chipExpandMore: "展开 {n}",
53
+ chipCollapse: "收起",
54
+ modelAuditTitle: "{n} 个模型没写能力字段",
55
+ modelAuditDesc: "没写的字段,pi 会按上下文 {ctx}、输出上限 {out}、纯文本处理。端点没报的值不会替你猜:照模型的真实规格填上(支持 256K / 1M 写法),保存时只补空着的格子,已写的值不改。",
56
+ modelAuditColModel: "模型",
57
+ modelAuditColContext: "上下文",
58
+ modelAuditColOutput: "输出上限",
59
+ modelAuditColVision: "视觉",
60
+ modelAuditVisionQuick: "视觉一起设:",
61
+ modelAuditVisionOn: "都有视觉",
62
+ modelAuditVisionOff: "都是纯文本",
63
+ modelAuditSave: "保存 {n} 个模型",
64
+ modelAuditSaveIdle: "填好后保存",
42
65
  capacityInvalid: "第 {n} 行:容量需为数字(可用 K/M)",
43
66
  discoverEmpty: "远端没有返回可识别的模型",
44
67
  candidateTitle: "发现 {count} 个模型,勾选要添加的",
45
68
  candidateAll: "全选",
46
69
  candidatePicked: "已选 {n} 个",
47
70
  candidateAdopt: "添加 {n} 个",
71
+ candidateInCatalog: "已在目录中",
48
72
  runningTool: "Running {name}…",
49
73
  thinkingLive: "Thinking",
50
74
  stepRunning: "Running",
@@ -127,6 +151,10 @@ const zh = {
127
151
  gitNoChanges: "工作区干净,没有变更",
128
152
  gitStatusUnknown: "Git 状态读取失败,无法确认是否有变更",
129
153
  gitRecentCommits: "最近提交",
154
+ gitStagingArea: "暂存区",
155
+ gitWorkingTree: "工作区",
156
+ gitGithubAccount: "GitHub 账号",
157
+ gitGithubUnknown: "未识别到 GitHub 账号",
130
158
  toolNoOutput: "无输出",
131
159
  toolFailed: "失败",
132
160
  thinkingClamped: "当前模型不支持「{requested}」,已改为「{level}」",
@@ -285,6 +313,11 @@ const zh = {
285
313
  extensionStatus: "扩展状态",
286
314
  contextUsed: "上下文已用",
287
315
  contextUnknown: "上下文用量未知",
316
+ contextEstimated: "上下文估算",
317
+ contextApiInput: "最近请求输入(API)",
318
+ contextCharacterBreakdown: "内容分类(字符数)",
319
+ contextCharacterHint: "按当前文本统计,图片不计。API 未提供分类 token 用量。",
320
+ contextChars: "字符",
288
321
  contextSystem: "系统提示词",
289
322
  contextMessages: "对话消息",
290
323
  contextTools: "工具",
@@ -326,6 +359,20 @@ const zh = {
326
359
  contextColdHint: "会话还没启动,System Prompt 和工具定义要启动后才算得出",
327
360
  contextCountNow: "现在统计",
328
361
  contextCounting: "统计中…",
362
+ contextWindowEdit: "修改",
363
+ contextWindowBuiltin: "pi 内置目录:{value}",
364
+ contextWindowUseBuiltin: "填入",
365
+ contextWindowScope: "改的是这个模型的全局配置(models.json),pi 终端和其它会话一起生效",
366
+ contextWindowOverBuiltin: "比内置目录大:超过模型实际上限时,请求会被上游拒绝(400)",
367
+ contextWindowNoBuiltin: "填模型实际支持的上限;填大了,请求会被上游拒绝(400)",
368
+ contextWindowInvalid: "填正整数,可带 K / M(如 200K)",
369
+ // @ 文件引用(输入 @ 弹候选)
370
+ mentionFiles: "@ 引用文件 / 文件夹",
371
+ mentionHint: "↑↓ 选择 · Enter 引用 · Esc 关闭",
372
+ mentionNoMatch: "没有匹配的文件",
373
+ mentionTruncated: "候选已截断",
374
+ mentionDiskSource: "目录遍历",
375
+ mentionDiskSourceHint: "当前工作区不是 git 仓库(或没装 git):候选来自目录遍历,会跳过 node_modules、构建产物等目录;列为 git 仓库后候选会自动按 .gitignore 过滤",
329
376
  // 统计条
330
377
  turns: "轮次",
331
378
  toolCalls: "工具调用",
@@ -334,6 +381,8 @@ const zh = {
334
381
  cacheHits: "缓存命中",
335
382
  statsCounts: "{turns} 轮 · {steps} 步",
336
383
  ttftAverage: "首 token 平均",
384
+ statsMeasuredSteps: "实时测量 {n} 步",
385
+ statsTimedCoverage: "{n}/{total} 条有计时",
337
386
  inputTokens: "输入",
338
387
  outputTokens: "输出",
339
388
  // 视图
@@ -507,10 +556,14 @@ const zh = {
507
556
  allowPrivateDiscovery: "允许本机/私网",
508
557
  allowPrivateHint: "模型地址指向本机或私有网络(如 Ollama、LM Studio、one-api 本地网关)时勾选,本次探测放行;发消息不受此限制",
509
558
  discoveringModels: "获取中...",
559
+ autoDiscovering: "正在自动识别模型…",
560
+ autoDiscovered: "已自动识别 {n} 个模型(端点报了容量与视觉的已一并填好)",
561
+ autoDiscoverFailed: "自动识别失败,可点「获取可用模型」重试",
510
562
  usingDefaultModels: "正在使用 Pi SDK 默认模型 · {count} 个",
511
563
  customModelsRequired: "至少添加一个可发送的模型 ID",
512
564
  modelCatalogEmptyBuiltin: "未添加覆盖模型;模型选择器会继续实时使用 Pi SDK 的官方模型目录。",
513
565
  modelCatalogEmptyCustom: "模型选择器中将不显示任何模型;请至少添加一个模型 ID。",
566
+ catalogOverrideHint: "与内置目录同 ID 的行只覆盖填写的字段,推理、价格、思考档位等沿用内置值。",
514
567
  addModel: "添加模型",
515
568
  modelId: "模型 ID",
516
569
  createProvider: "创建提供方",
@@ -723,13 +776,37 @@ const en: Dict = {
723
776
  capacityFields: "Capacities",
724
777
  contextWindowLabel: "Context window",
725
778
  maxTokensLabel: "Max output",
726
- capacityHint: "blank = default · 256K / 2M accepted",
779
+ capacityHint: "blank = pi default (128K / 16K) · 256K / 2M accepted",
780
+ // model capability (vision) and config checkup
781
+ visionLabel: "Vision",
782
+ visionHint: "Whether this model accepts images. Left unset, pi treats it as text-only and replaces pasted screenshots with a placeholder line.",
783
+ visionUnset: "Not declared",
784
+ visionOn: "Vision",
785
+ visionOff: "Text only",
786
+ imagePreview: "Click to preview",
787
+ imageViewer: "Image preview",
788
+ imagePrev: "Previous image",
789
+ imageNext: "Next image",
790
+ chipExpandMore: "Show {n} more",
791
+ chipCollapse: "Collapse",
792
+ modelAuditTitle: "{n} models don't declare their capabilities",
793
+ modelAuditDesc: "For fields left out, pi assumes a {ctx} context window, {out} max output and text-only input. Values the endpoint didn't report are never guessed: fill in the model's real limits (256K / 1M accepted). Saving only fills empty cells and never changes values already set.",
794
+ modelAuditColModel: "Model",
795
+ modelAuditColContext: "Context",
796
+ modelAuditColOutput: "Max output",
797
+ modelAuditColVision: "Vision",
798
+ modelAuditVisionQuick: "Set vision for all:",
799
+ modelAuditVisionOn: "All vision",
800
+ modelAuditVisionOff: "All text only",
801
+ modelAuditSave: "Save {n} models",
802
+ modelAuditSaveIdle: "Fill in, then save",
727
803
  capacityInvalid: "Row {n}: capacity must be a number (K/M ok)",
728
804
  discoverEmpty: "No recognizable models returned",
729
805
  candidateTitle: "Found {count} models — pick ones to add",
730
806
  candidateAll: "Select all",
731
807
  candidatePicked: "{n} selected",
732
808
  candidateAdopt: "Add {n}",
809
+ candidateInCatalog: "Already in catalog",
733
810
  runningTool: "Running {name}…",
734
811
  thinkingLive: "Thinking",
735
812
  stepRunning: "Running",
@@ -812,6 +889,10 @@ const en: Dict = {
812
889
  gitNoChanges: "Working tree clean",
813
890
  gitStatusUnknown: "Git status unavailable; changes could not be determined",
814
891
  gitRecentCommits: "Recent commits",
892
+ gitStagingArea: "Staging area",
893
+ gitWorkingTree: "Working tree",
894
+ gitGithubAccount: "GitHub account",
895
+ gitGithubUnknown: "No GitHub account detected",
815
896
  toolNoOutput: "no output",
816
897
  toolFailed: "failed",
817
898
  thinkingClamped: "This model does not support \"{requested}\"; using \"{level}\" instead",
@@ -968,6 +1049,11 @@ const en: Dict = {
968
1049
  extensionStatus: "Extension status",
969
1050
  contextUsed: "context used",
970
1051
  contextUnknown: "Context usage unknown",
1052
+ contextEstimated: "estimated context usage",
1053
+ contextApiInput: "Last request input (API)",
1054
+ contextCharacterBreakdown: "Content breakdown (characters)",
1055
+ contextCharacterHint: "Counts current text; excludes images. The API does not report tokens by category.",
1056
+ contextChars: "chars",
971
1057
  contextSystem: "System prompt",
972
1058
  contextMessages: "Messages",
973
1059
  contextTools: "Tools",
@@ -1009,6 +1095,20 @@ const en: Dict = {
1009
1095
  contextColdHint: "Session not started yet — System Prompt and tool definitions are counted once it starts",
1010
1096
  contextCountNow: "Count now",
1011
1097
  contextCounting: "Counting…",
1098
+ contextWindowEdit: "Edit",
1099
+ contextWindowBuiltin: "pi built-in catalog: {value}",
1100
+ contextWindowUseBuiltin: "Use",
1101
+ contextWindowScope: "Changes this model's global config (models.json); the pi terminal and other sessions use it too",
1102
+ contextWindowOverBuiltin: "Larger than the built-in catalog: requests past the model's real limit are rejected upstream (400)",
1103
+ contextWindowNoBuiltin: "Enter the model's real limit; a larger value gets requests rejected upstream (400)",
1104
+ contextWindowInvalid: "Enter a positive integer; K / M allowed (e.g. 200K)",
1105
+ // @ file references (type @ to open the picker)
1106
+ mentionFiles: "@ reference files / folders",
1107
+ mentionHint: "↑↓ select · Enter reference · Esc close",
1108
+ mentionNoMatch: "No matching files",
1109
+ mentionTruncated: "list truncated",
1110
+ mentionDiskSource: "directory scan",
1111
+ mentionDiskSourceHint: "This workspace is not a git repository (or git is missing): candidates come from a directory scan that skips node_modules and build output; in a git repository they follow .gitignore automatically",
1012
1112
  turns: "turns",
1013
1113
  toolCalls: "tool calls",
1014
1114
  cost: "cost",
@@ -1016,6 +1116,8 @@ const en: Dict = {
1016
1116
  cacheHits: "cache hits",
1017
1117
  statsCounts: "{turns} turns · {steps} steps",
1018
1118
  ttftAverage: "TTFT avg",
1119
+ statsMeasuredSteps: "{n} steps measured live",
1120
+ statsTimedCoverage: "{n}/{total} entries timed",
1019
1121
  inputTokens: "Input",
1020
1122
  outputTokens: "Output",
1021
1123
  tabChat: "Chat",
@@ -1185,10 +1287,14 @@ const en: Dict = {
1185
1287
  allowPrivateDiscovery: "Allow local/private",
1186
1288
  allowPrivateHint: "Check this when the model address points to loopback or a private network (Ollama, LM Studio, one-api gateways) to allow this probe; chat requests are not affected",
1187
1289
  discoveringModels: "Fetching...",
1290
+ autoDiscovering: "Detecting models…",
1291
+ autoDiscovered: "Detected {n} models (capacity and vision filled in where the endpoint reports them)",
1292
+ autoDiscoverFailed: "Automatic detection failed — use the button to retry",
1188
1293
  usingDefaultModels: "Using Pi SDK default models · {count}",
1189
1294
  customModelsRequired: "Add at least one model ID that can be sent",
1190
1295
  modelCatalogEmptyBuiltin: "No model overrides. The model selector will continue using Pi SDK's live official catalog.",
1191
1296
  modelCatalogEmptyCustom: "No models will appear in the selector. Add at least one model ID.",
1297
+ catalogOverrideHint: "Rows matching a built-in model ID only override the fields you fill in; reasoning, pricing and thinking levels stay built-in.",
1192
1298
  addModel: "Add model",
1193
1299
  modelId: "Model ID",
1194
1300
  createProvider: "Create provider",