@rexvane/piweb 0.3.16 → 0.3.18

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 (288) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +8 -11
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +6 -6
  5. package/.next/react-loadable-manifest.json +4 -16
  6. package/.next/required-server-files.js +1 -3
  7. package/.next/required-server-files.json +1 -3
  8. package/.next/routes-manifest.json +0 -20
  9. package/.next/server/app/_global-error/page.js +2 -2
  10. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  11. package/.next/server/app/_global-error.html +1 -1
  12. package/.next/server/app/_global-error.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/server/app/_not-found/page.js +1 -1
  17. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  18. package/.next/server/app/_not-found.html +1 -1
  19. package/.next/server/app/_not-found.rsc +4 -7
  20. package/.next/server/app/_not-found.segments/_full.segment.rsc +4 -7
  21. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -5
  22. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -5
  23. package/.next/server/app/api/agent/[id]/events/route.js +2 -2
  24. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
  25. package/.next/server/app/api/agent/[id]/route.js +1 -1
  26. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
  27. package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
  28. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
  29. package/.next/server/app/api/agent/new/route.js +1 -1
  30. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  31. package/.next/server/app/api/config/route.js +1 -1
  32. package/.next/server/app/api/config/route.js.nft.json +1 -1
  33. package/.next/server/app/api/files/route.js +1 -1
  34. package/.next/server/app/api/files/route.js.nft.json +1 -1
  35. package/.next/server/app/api/git/route.js +2 -2
  36. package/.next/server/app/api/git/route.js.nft.json +1 -1
  37. package/.next/server/app/api/growth/route.js +1 -1
  38. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  39. package/.next/server/app/api/health/route.js +1 -1
  40. package/.next/server/app/api/models/route.js +1 -1
  41. package/.next/server/app/api/models/route.js.nft.json +1 -1
  42. package/.next/server/app/api/pi-settings/route.js +1 -1
  43. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  44. package/.next/server/app/api/plugins/route.js +1 -1
  45. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  46. package/.next/server/app/api/prompts/route.js +1 -1
  47. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  48. package/.next/server/app/api/security/route.js +1 -1
  49. package/.next/server/app/api/security/route.js.nft.json +1 -1
  50. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  51. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  52. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  53. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  54. package/.next/server/app/api/sessions/import/route.js +3 -3
  55. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
  56. package/.next/server/app/api/sessions/route.js +1 -1
  57. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  58. package/.next/server/app/api/skills/route.js +2 -2
  59. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  60. package/.next/server/app/api/update/route.js +2 -2
  61. package/.next/server/app/api/update/route.js.nft.json +1 -1
  62. package/.next/server/app/api/version/route.js +1 -1
  63. package/.next/server/app/api/version/route.js.nft.json +1 -1
  64. package/.next/server/app/api/web-auth/route.js +1 -1
  65. package/.next/server/app/api/workspaces/route.js +1 -1
  66. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  67. package/.next/server/app/icon.svg/route.js +1 -1
  68. package/.next/server/app/index.html +1 -1
  69. package/.next/server/app/index.rsc +6 -9
  70. package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -7
  71. package/.next/server/app/index.segments/_full.segment.rsc +6 -9
  72. package/.next/server/app/index.segments/_tree.segment.rsc +2 -5
  73. package/.next/server/app/login/page.js +1 -1
  74. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  75. package/.next/server/app/login.html +1 -1
  76. package/.next/server/app/login.rsc +5 -8
  77. package/.next/server/app/login.segments/_full.segment.rsc +5 -8
  78. package/.next/server/app/login.segments/_tree.segment.rsc +2 -5
  79. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +3 -6
  80. package/.next/server/app/page.js +1 -1
  81. package/.next/server/app/page_client-reference-manifest.js +1 -1
  82. package/.next/server/app-paths-manifest.json +8 -11
  83. package/.next/server/chunks/496.js +1 -1
  84. package/.next/server/chunks/532.js +1 -1
  85. package/.next/server/chunks/686.js +4 -8
  86. package/.next/server/middleware-build-manifest.js +1 -1
  87. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  88. package/.next/server/pages/404.html +1 -1
  89. package/.next/server/pages/500.html +1 -1
  90. package/.next/server/server-reference-manifest.json +1 -1
  91. package/.next/static/chunks/2608.ec8ff1f5767c9056.js +1 -0
  92. package/.next/static/chunks/2716.93be8ad5c970c516.js +1 -0
  93. package/.next/static/chunks/4632.112d2e893d48d188.js +1 -0
  94. package/.next/static/chunks/4673.c1fb1580efc6ace7.js +1 -0
  95. package/.next/static/chunks/4686-f4a037d609cea3f2.js +1 -0
  96. package/.next/static/chunks/app/_global-error/page-f77e9ede9808b18e.js +1 -0
  97. package/.next/static/chunks/app/api/agent/[id]/events/route-f77e9ede9808b18e.js +1 -0
  98. package/.next/static/chunks/app/api/agent/[id]/route-f77e9ede9808b18e.js +1 -0
  99. package/.next/static/chunks/app/api/agent/[id]/tree/route-f77e9ede9808b18e.js +1 -0
  100. package/.next/static/chunks/app/api/agent/new/route-f77e9ede9808b18e.js +1 -0
  101. package/.next/static/chunks/app/api/config/route-f77e9ede9808b18e.js +1 -0
  102. package/.next/static/chunks/app/api/files/route-f77e9ede9808b18e.js +1 -0
  103. package/.next/static/chunks/app/api/git/route-f77e9ede9808b18e.js +1 -0
  104. package/.next/static/chunks/app/api/growth/route-f77e9ede9808b18e.js +1 -0
  105. package/.next/static/chunks/app/api/health/route-f77e9ede9808b18e.js +1 -0
  106. package/.next/static/chunks/app/api/models/route-f77e9ede9808b18e.js +1 -0
  107. package/.next/static/chunks/app/api/pi-settings/route-f77e9ede9808b18e.js +1 -0
  108. package/.next/static/chunks/app/api/plugins/route-f77e9ede9808b18e.js +1 -0
  109. package/.next/static/chunks/app/api/prompts/route-f77e9ede9808b18e.js +1 -0
  110. package/.next/static/chunks/app/api/security/route-f77e9ede9808b18e.js +1 -0
  111. package/.next/static/chunks/app/api/sessions/[id]/export/route-f77e9ede9808b18e.js +1 -0
  112. package/.next/static/chunks/app/api/sessions/[id]/route-f77e9ede9808b18e.js +1 -0
  113. package/.next/static/chunks/app/api/sessions/import/route-f77e9ede9808b18e.js +1 -0
  114. package/.next/static/chunks/app/api/sessions/route-f77e9ede9808b18e.js +1 -0
  115. package/.next/static/chunks/app/api/skills/route-f77e9ede9808b18e.js +1 -0
  116. package/.next/static/chunks/app/api/update/route-f77e9ede9808b18e.js +1 -0
  117. package/.next/static/chunks/app/api/version/route-f77e9ede9808b18e.js +1 -0
  118. package/.next/static/chunks/app/api/web-auth/route-f77e9ede9808b18e.js +1 -0
  119. package/.next/static/chunks/app/api/workspaces/route-f77e9ede9808b18e.js +1 -0
  120. package/.next/static/chunks/app/page-b51087831582ee37.js +1 -0
  121. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f77e9ede9808b18e.js +1 -0
  122. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f77e9ede9808b18e.js +1 -0
  123. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f77e9ede9808b18e.js +1 -0
  124. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f77e9ede9808b18e.js +1 -0
  125. package/.next/static/chunks/webpack-c31263fd83d75ce6.js +1 -0
  126. package/.next/static/css/0cd968b9b62035b5.css +1 -0
  127. package/.next/static/en03hnPiuro0rnf8QkFEe/_buildManifest.js +1 -0
  128. package/.next/types/routes.d.ts +1 -4
  129. package/.next/types/validator.ts +0 -27
  130. package/README.md +17 -29
  131. package/assets/archive.jpg +0 -0
  132. package/assets/growth-1.jpg +0 -0
  133. package/assets/growth-2.jpg +0 -0
  134. package/assets/import.jpg +0 -0
  135. package/assets/main.jpg +0 -0
  136. package/assets/outline.jpg +0 -0
  137. package/assets/prompts-1.jpg +0 -0
  138. package/assets/prompts-2.jpg +0 -0
  139. package/assets/providers-1.jpg +0 -0
  140. package/assets/providers-2.jpg +0 -0
  141. package/assets/providers-3.jpg +0 -0
  142. package/assets/trace-1.jpg +0 -0
  143. package/assets/trace-2.png +0 -0
  144. package/assets/trace-3.png +0 -0
  145. package/assets/trace-4.png +0 -0
  146. package/assets/viewer-1.jpg +0 -0
  147. package/assets/viewer-2.jpg +0 -0
  148. package/bin/pi.js +50 -0
  149. package/next.config.ts +32 -7
  150. package/package.json +4 -9
  151. package/scripts/launcher.mjs +1 -1
  152. package/scripts/pick-folder.ps1 +2 -11
  153. package/scripts/release.mjs +31 -11
  154. package/scripts/verify-package.mjs +0 -53
  155. package/src/app/api/config/route.ts +1 -4
  156. package/src/app/api/growth/route.ts +14 -14
  157. package/src/app/api/models/route.ts +2 -1
  158. package/src/app/api/security/route.ts +3 -8
  159. package/src/app/api/update/route.ts +2 -8
  160. package/src/app/globals.css +92 -777
  161. package/src/app/layout.tsx +6 -13
  162. package/src/components/AppShell.tsx +229 -521
  163. package/src/components/ChatInput.tsx +26 -153
  164. package/src/components/ChatWindow.tsx +231 -51
  165. package/src/components/ContextMeter.tsx +17 -11
  166. package/src/components/FileViewer.tsx +11 -10
  167. package/src/components/GitPanel.tsx +11 -13
  168. package/src/components/ModelSelector.tsx +6 -19
  169. package/src/components/PiMark.tsx +38 -5
  170. package/src/components/ProjectPanel.tsx +111 -95
  171. package/src/components/PromptPanel.tsx +51 -132
  172. package/src/components/SessionImportSection.tsx +37 -53
  173. package/src/components/SessionSidebar.tsx +57 -159
  174. package/src/components/SettingsPanel.tsx +193 -330
  175. package/src/components/icons.tsx +0 -23
  176. package/src/hooks/useGrowth.ts +123 -156
  177. package/src/hooks/usePiWeb.ts +15 -122
  178. package/src/i18n.tsx +42 -98
  179. package/src/lib/agent-manager.ts +21 -187
  180. package/src/lib/command-validation.ts +1 -9
  181. package/src/lib/growth-service.ts +390 -166
  182. package/src/lib/growth-tracker.ts +64 -264
  183. package/src/lib/growth-turns.ts +31 -1
  184. package/src/lib/models/model-scope.ts +15 -0
  185. package/src/lib/models-service.ts +16 -0
  186. package/src/lib/pi-settings.ts +7 -0
  187. package/src/lib/pi.ts +2 -3
  188. package/src/lib/session-import/claude.ts +2 -13
  189. package/src/lib/session-import/codex.ts +8 -44
  190. package/src/lib/session-import/files.ts +0 -14
  191. package/src/lib/session-import/grok.ts +24 -54
  192. package/src/lib/session-import/index.ts +0 -5
  193. package/src/lib/session-import/types.ts +0 -12
  194. package/src/lib/session-import/writer.ts +3 -8
  195. package/src/lib/skills-service.ts +9 -16
  196. package/src/lib/theme.ts +1 -68
  197. package/src/lib/trajectory.ts +0 -1
  198. package/src/lib/types.ts +27 -36
  199. package/tsconfig.json +2 -1
  200. package/.next/server/app/api/swarm/[id]/route.js +0 -1
  201. package/.next/server/app/api/swarm/[id]/route.js.nft.json +0 -1
  202. package/.next/server/app/api/swarm/[id]/route_client-reference-manifest.js +0 -1
  203. package/.next/server/app/api/swarm/route.js +0 -1
  204. package/.next/server/app/api/swarm/route.js.nft.json +0 -1
  205. package/.next/server/app/api/swarm/route_client-reference-manifest.js +0 -1
  206. package/.next/server/app/api/tests/route.js +0 -2
  207. package/.next/server/app/api/tests/route.js.nft.json +0 -1
  208. package/.next/server/app/api/tests/route_client-reference-manifest.js +0 -1
  209. package/.next/server/chunks/757.js +0 -1
  210. package/.next/server/chunks/775.js +0 -3
  211. package/.next/static/0t1jBUAuVaTFTXxRyIo3q/_buildManifest.js +0 -1
  212. package/.next/static/chunks/2608.43c58236c9cce8e6.js +0 -1
  213. package/.next/static/chunks/2716.4ec42df1d51354c9.js +0 -1
  214. package/.next/static/chunks/4632.62139b565b657aac.js +0 -1
  215. package/.next/static/chunks/4673.7796104395deb8d7.js +0 -1
  216. package/.next/static/chunks/4686-17f21a006739a596.js +0 -1
  217. package/.next/static/chunks/7577.10af9bb661652da8.js +0 -1
  218. package/.next/static/chunks/8866.38ec4ac4ee5a1b74.js +0 -1
  219. package/.next/static/chunks/app/_global-error/page-5148173b3f55e415.js +0 -1
  220. package/.next/static/chunks/app/api/agent/[id]/events/route-5148173b3f55e415.js +0 -1
  221. package/.next/static/chunks/app/api/agent/[id]/route-5148173b3f55e415.js +0 -1
  222. package/.next/static/chunks/app/api/agent/[id]/tree/route-5148173b3f55e415.js +0 -1
  223. package/.next/static/chunks/app/api/agent/new/route-5148173b3f55e415.js +0 -1
  224. package/.next/static/chunks/app/api/config/route-5148173b3f55e415.js +0 -1
  225. package/.next/static/chunks/app/api/files/route-5148173b3f55e415.js +0 -1
  226. package/.next/static/chunks/app/api/git/route-5148173b3f55e415.js +0 -1
  227. package/.next/static/chunks/app/api/growth/route-5148173b3f55e415.js +0 -1
  228. package/.next/static/chunks/app/api/health/route-5148173b3f55e415.js +0 -1
  229. package/.next/static/chunks/app/api/models/route-5148173b3f55e415.js +0 -1
  230. package/.next/static/chunks/app/api/pi-settings/route-5148173b3f55e415.js +0 -1
  231. package/.next/static/chunks/app/api/plugins/route-5148173b3f55e415.js +0 -1
  232. package/.next/static/chunks/app/api/prompts/route-5148173b3f55e415.js +0 -1
  233. package/.next/static/chunks/app/api/security/route-5148173b3f55e415.js +0 -1
  234. package/.next/static/chunks/app/api/sessions/[id]/export/route-5148173b3f55e415.js +0 -1
  235. package/.next/static/chunks/app/api/sessions/[id]/route-5148173b3f55e415.js +0 -1
  236. package/.next/static/chunks/app/api/sessions/import/route-5148173b3f55e415.js +0 -1
  237. package/.next/static/chunks/app/api/sessions/route-5148173b3f55e415.js +0 -1
  238. package/.next/static/chunks/app/api/skills/route-5148173b3f55e415.js +0 -1
  239. package/.next/static/chunks/app/api/swarm/[id]/route-5148173b3f55e415.js +0 -1
  240. package/.next/static/chunks/app/api/swarm/route-5148173b3f55e415.js +0 -1
  241. package/.next/static/chunks/app/api/tests/route-5148173b3f55e415.js +0 -1
  242. package/.next/static/chunks/app/api/update/route-5148173b3f55e415.js +0 -1
  243. package/.next/static/chunks/app/api/version/route-5148173b3f55e415.js +0 -1
  244. package/.next/static/chunks/app/api/web-auth/route-5148173b3f55e415.js +0 -1
  245. package/.next/static/chunks/app/api/workspaces/route-5148173b3f55e415.js +0 -1
  246. package/.next/static/chunks/app/page-14379b8060d5cf07.js +0 -1
  247. package/.next/static/chunks/next/dist/client/components/builtin/app-error-5148173b3f55e415.js +0 -1
  248. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-5148173b3f55e415.js +0 -1
  249. package/.next/static/chunks/next/dist/client/components/builtin/not-found-5148173b3f55e415.js +0 -1
  250. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-5148173b3f55e415.js +0 -1
  251. package/.next/static/chunks/webpack-d1a69ad9edb38b11.js +0 -1
  252. package/.next/static/css/d0a0162cb481abe8.css +0 -1
  253. package/.next/types/app/api/swarm/[id]/route.ts +0 -353
  254. package/.next/types/app/api/swarm/route.ts +0 -353
  255. package/.next/types/app/api/tests/route.ts +0 -353
  256. package/assets/archive.png +0 -0
  257. package/assets/growth-1.png +0 -0
  258. package/assets/growth-2.png +0 -0
  259. package/assets/import.png +0 -0
  260. package/assets/main.png +0 -0
  261. package/assets/outline.png +0 -0
  262. package/assets/prompts-1.png +0 -0
  263. package/assets/prompts-2.png +0 -0
  264. package/assets/providers-1.png +0 -0
  265. package/assets/providers-2.png +0 -0
  266. package/assets/providers-3.png +0 -0
  267. package/assets/trace-1.png +0 -0
  268. package/assets/viewer-1.png +0 -0
  269. package/assets/viewer-2.png +0 -0
  270. package/scripts/managed-niubash-assets.mjs +0 -146
  271. package/scripts/prepare-niubash-packages.mjs +0 -182
  272. package/scripts/run-test-file.mjs +0 -34
  273. package/scripts/smoke-managed-niubash.mjs +0 -62
  274. package/scripts/verify-release-config.mjs +0 -24
  275. package/src/app/api/swarm/[id]/route.ts +0 -28
  276. package/src/app/api/swarm/route.ts +0 -24
  277. package/src/app/api/tests/route.ts +0 -27
  278. package/src/components/SwarmCoordinatorPod.tsx +0 -1021
  279. package/src/components/TestInspectorPod.tsx +0 -549
  280. package/src/lib/delegation.ts +0 -93
  281. package/src/lib/growth-rounds.ts +0 -84
  282. package/src/lib/managed-niubash.ts +0 -231
  283. package/src/lib/runtime-activity.ts +0 -27
  284. package/src/lib/session-import/lock.ts +0 -25
  285. package/src/lib/swarm-service.ts +0 -371
  286. package/src/lib/test-run-service.ts +0 -122
  287. package/src/lib/workflow.ts +0 -55
  288. /package/.next/static/{0t1jBUAuVaTFTXxRyIo3q → en03hnPiuro0rnf8QkFEe}/_ssgManifest.js +0 -0
@@ -1,88 +1,57 @@
1
1
  /**
2
- * growth-tracker:把 pi 会话事件接到项目生长快照引擎上。
2
+ * growth-tracker:把 pi 会话事件接到「每轮一个 git commit」上。
3
3
  *
4
- * - agent_start:确保本会话有基线步,并启动工作区目录监听
5
- * - 改盘类工具(bash / powershell / write / edit / 未知扩展工具)结束:去抖 200ms 拍一张快照(并行工具合并成一步)
6
- * - agent_end:兜底再拍一次(tree 没变就不记步)
7
- * - 目录监听:改盘类工具运行期间看到的新路径先以「生成中」推给浏览器;空闲时的改动去抖 1.5s 记成「外部修改」
8
- * 快照失败不影响 pi,只向浏览器报一次错;项目过大 / 没有 git 时本会话停用。
4
+ * - prepare()(发 prompt 前):把两轮之间用户自己的修改单独提交一次,不混进 pi 这一轮
5
+ * (工作区还没有任何提交时记成基线)
6
+ * - agent_start → agent_settled 是一轮:结束时提交本轮,提问首行做标题、用户消息 entryId 写进 trailers;
7
+ * 没有改动也提交空 commit,轮号与对话一一对应
8
+ * 提交失败不影响 pi,只向浏览器报一次错;项目过大 / 没有 git 时本会话停用。
9
9
  */
10
- import fs from "node:fs";
11
- import { realpathSync } from "node:fs";
12
- import path from "node:path";
13
10
  import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
14
- import { GrowthError, hasSessionSteps, isExcludedRelPath, snapshot } from "./growth-service";
15
- import type { GrowthStep, GrowthStepKind, WebEvent } from "./types";
11
+ import { GrowthError, commitRound, recordWorkspaceChanges } from "./growth-service";
12
+ import { promptsFromEntries } from "./growth-turns";
13
+ import type { GrowthRound, GrowthRoundStatus, WebEvent } from "./types";
16
14
 
17
- const READ_ONLY_TOOLS = new Set(["read", "grep", "find", "ls"]);
18
- const SNAPSHOT_DEBOUNCE_MS = 200;
19
- const PENDING_THROTTLE_MS = 300;
20
- const EXTERNAL_DEBOUNCE_MS = 1500;
21
- const MAX_PENDING = 500;
22
15
  const ERROR_THROTTLE_MS = 60_000;
23
- export const GROWTH_TRACKER_VERSION = 3;
16
+ export const GROWTH_TRACKER_VERSION = 4;
24
17
 
25
- interface QueuedMeta {
26
- kind: GrowthStepKind;
27
- label: string;
28
- toolCallId?: string;
29
- toolName?: string;
30
- /** 归属时刻:轮次内的步传轮次开始时间,避免慢快照落账晚被切进下一轮窗口 */
31
- ts?: number;
18
+ interface SessionEntryLike {
19
+ type?: string;
20
+ id?: string;
21
+ message?: { role?: string; content?: unknown; stopReason?: string };
32
22
  }
33
23
 
34
24
  export interface GrowthTracker {
35
25
  version: number;
36
26
  getError(): string | null;
37
- /** 在首轮 prompt 前完成基线,避免快速写盘被误算进基线。 */
27
+ /** 发 prompt 前完成「你的修改」提交,避免它被算进 pi 这一轮 */
38
28
  prepare(): Promise<void>;
39
29
  onEvent(evt: AgentSessionEvent): void;
40
- /** 手动快照(API 触发);返回记下的步,tree 没变返回 null */
41
- snapshotNow(label?: string): Promise<GrowthStep | null>;
30
+ /** 立即记录(项目栏按钮):把当前改动提交为「你的修改」;没改动返回 null */
31
+ recordNow(): Promise<GrowthRound | null>;
42
32
  dispose(): void;
43
33
  }
44
34
 
45
- export function isMutatingTool(name: string): boolean {
46
- return !READ_ONLY_TOOLS.has(name.toLowerCase());
35
+ /** 本轮最后一条助手消息的结束原因 → 轮次状态(用户中止 / 模型报错 / 正常) */
36
+ function roundStatus(entries: readonly SessionEntryLike[]): GrowthRoundStatus {
37
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
38
+ const message = entries[i].type === "message" ? entries[i].message : undefined;
39
+ if (message?.role !== "assistant") continue;
40
+ if (message.stopReason === "aborted") return "aborted";
41
+ if (message.stopReason === "error") return "error";
42
+ return "done";
43
+ }
44
+ return "done";
47
45
  }
48
46
 
49
- /** 「bash · mkdir -p src」:工具名 + 命令首行 / 相对路径,截到 60 字 */
50
- export function labelForTool(cwd: string, name: string, args: unknown): string {
51
- const a = args && typeof args === "object" ? (args as Record<string, unknown>) : {};
52
- let detail = "";
53
- if (typeof a.command === "string") detail = a.command;
54
- else if (typeof a.path === "string") detail = relativeToCwd(cwd, a.path);
55
- else if (typeof a.file_path === "string") detail = relativeToCwd(cwd, a.file_path);
56
- detail = detail.split("\n").map((s) => s.trim()).find(Boolean) ?? "";
57
- detail = detail.replace(/\s+/g, " ");
58
- if (detail.length > 60) detail = `${detail.slice(0, 59)}…`;
59
- return detail ? `${name} · ${detail}` : name;
60
- }
61
-
62
- function relativeToCwd(cwd: string, p: string): string {
63
- if (!path.isAbsolute(p)) return p.replace(/\\/g, "/");
64
- const rel = path.relative(cwd, p);
65
- if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) return p.replace(/\\/g, "/");
66
- return rel.replace(/\\/g, "/");
67
- }
68
-
69
- /** 工具链自动生成/频繁改写的文件:进 pending 只会制造蓝点噪音,快照侧由 .gitignore 与影子仓库 exclude 兜底 */
70
- const WATCHER_FILE_NOISE = /(?:^|\/)(?:\.DS_Store|Thumbs\.db|[^/]*\.log|[^/]*\.tsbuildinfo|next-env\.d\.ts)$/;
71
-
72
- export function createGrowthTracker(opts: { cwd: string; sessionPath: string; publish: (evt: WebEvent) => void }): GrowthTracker {
73
- const { cwd, sessionPath, publish } = opts;
74
- const running = new Set<string>();
75
- const argsById = new Map<string, unknown>();
76
- const queue: QueuedMeta[] = [];
77
- const pending = new Set<string>();
78
- let pendingStats = 0;
79
- let flushTimer: ReturnType<typeof setTimeout> | undefined;
80
- let pendingTimer: ReturnType<typeof setTimeout> | undefined;
81
- let externalTimer: ReturnType<typeof setTimeout> | undefined;
82
- let watcher: fs.FSWatcher | null = null;
83
- let watcherFailed = false;
84
- let baselineChecked = false;
85
- let baselinePending: Promise<void> | null = null;
47
+ export function createGrowthTracker(opts: {
48
+ cwd: string;
49
+ sessionPath: string;
50
+ publish: (evt: WebEvent) => void;
51
+ /** 会话的持久条目(SessionManager.getEntries):轮结束时从中取本轮的提问 */
52
+ entries: () => readonly SessionEntryLike[];
53
+ }): GrowthTracker {
54
+ const { cwd, sessionPath, publish, entries } = opts;
86
55
  let disabled = false;
87
56
  let disabledFailure: GrowthError | null = null;
88
57
  let disposed = false;
@@ -90,243 +59,83 @@ export function createGrowthTracker(opts: { cwd: string; sessionPath: string; pu
90
59
  let errorActive = false;
91
60
  let lastErrorMessage: string | null = null;
92
61
  let chain: Promise<void> = Promise.resolve();
93
- /** 当前轮次的开始时刻:本轮所有步的归属 ts(慢快照不再把改动算给下一轮) */
94
- let turnTs: number | undefined;
62
+ /** 本轮开始时会话条目的数量;null = 不在一轮之中 */
63
+ let runStart: number | null = null;
95
64
 
96
- // 快照失败不打扰用户(项目栏退化成普通文件树 + 查看器):只在服务端日志记一次,没 git / 项目过大就停用本会话的跟踪
65
+ // 提交失败不打扰用户(项目栏退化成普通文件树 + 查看器):只在服务端日志记一次,没 git / 项目过大就停用本会话的跟踪
97
66
  const reportError = (error: unknown) => {
98
67
  const message = error instanceof Error ? error.message : String(error);
99
68
  const terminal = error instanceof GrowthError && (error.code === "unavailable" || error.code === "too-large");
100
69
  if (terminal) {
101
70
  disabled = true;
102
71
  disabledFailure = error;
103
- try {
104
- watcher?.close();
105
- } catch {
106
- /* ignore watcher close errors after a terminal snapshot failure */
107
- }
108
- watcher = null;
109
- if (flushTimer) clearTimeout(flushTimer);
110
- if (pendingTimer) clearTimeout(pendingTimer);
111
- if (externalTimer) clearTimeout(externalTimer);
112
- flushTimer = pendingTimer = externalTimer = undefined;
113
- queue.length = 0;
114
- running.clear();
115
- argsById.clear();
116
- if (pending.size) {
117
- pending.clear();
118
- publish({ type: "growth_pending", paths: [], ts: Date.now() });
119
- }
72
+ runStart = null;
120
73
  }
121
74
  errorActive = true;
122
75
  lastErrorMessage = message;
123
76
  const now = Date.now();
124
77
  if (!terminal && now - lastErrorAt < ERROR_THROTTLE_MS) return;
125
78
  lastErrorAt = now;
126
- console.warn(`[piweb] growth snapshot failed for ${cwd}: ${message}`);
79
+ console.warn(`[piweb] growth commit failed for ${cwd}: ${message}`);
127
80
  publish({ type: "growth_error", message, ts: now });
128
81
  };
129
82
 
130
- const publishPending = () => {
131
- pendingTimer = undefined;
132
- publish({ type: "growth_pending", paths: [...pending].slice(0, 200), ts: Date.now() });
133
- };
134
-
135
83
  const enqueue = (fn: () => Promise<void>) => {
136
84
  chain = chain.then(fn, fn).catch(reportError);
137
85
  return chain;
138
86
  };
139
87
 
140
- // 真正拍快照的函数不能再 enqueue(链上任务里嵌套排队会等自己,整条链永久卡死)
141
- const doRecord = async (meta: QueuedMeta, force = false) => {
142
- if (disabled || disposed) return;
143
- const step = await snapshot(cwd, { ...meta, session: sessionPath, force });
88
+ const published = (round: GrowthRound | null) => {
144
89
  if (errorActive) {
145
90
  errorActive = false;
146
91
  lastErrorMessage = null;
147
92
  publish({ type: "growth_error", message: null, ts: Date.now() });
148
93
  }
149
- if (!step || disposed) return;
150
- publish({ type: "growth", step, ts: Date.now() });
151
- if (pending.size) {
152
- pending.clear();
153
- if (pendingTimer) clearTimeout(pendingTimer);
154
- publishPending();
155
- }
156
- };
157
- const record = (meta: QueuedMeta, force = false) => enqueue(() => doRecord(meta, force));
158
-
159
- const ensureBaseline = (): Promise<void> => {
160
- if (baselineChecked) return chain;
161
- if (baselinePending) return baselinePending;
162
- baselinePending = enqueue(async () => {
163
- if (disabled || disposed) return;
164
- if (!(await hasSessionSteps(cwd, sessionPath))) await doRecord({ kind: "baseline", label: "", ts: turnTs }, true);
165
- baselineChecked = true;
166
- });
167
- void baselinePending.finally(() => { baselinePending = null; });
168
- return baselinePending;
169
- };
170
-
171
- const flush = () => {
172
- flushTimer = undefined;
173
- const metas = queue.splice(0);
174
- if (!metas.length) return;
175
- const tools = metas.filter((x) => x.kind === "tool");
176
- const head = tools[0] ?? metas[0];
177
- const extra = tools.length > 1 ? ` +${tools.length - 1}` : "";
178
- // 归属时刻优先用队里最新的(同轮多个工具取最后一个工具结束时也在轮内);
179
- // 没有就退回当前轮开始时刻,再退回落账时刻。
180
- const ts = metas.reduce<number | undefined>((acc, m) => m.ts ?? acc, undefined) ?? turnTs;
181
- // 首轮工具快照前确保基线已在链上排队:基线落账晚于首轮工具时,
182
- // buildGrowthRounds 按时间戳切窗会把首轮误判成「快照启用前,未记录」。
183
- const recordAfterBaseline = baselineChecked
184
- ? Promise.resolve()
185
- : ensureBaseline().catch(() => undefined);
186
- void recordAfterBaseline.then(() => {
187
- if (disposed || disabled) return;
188
- record({ kind: head.kind, label: `${head.label}${extra}`, toolCallId: head.toolCallId, toolName: head.toolName, ts });
189
- });
190
- };
191
-
192
- const schedule = (meta: QueuedMeta) => {
193
- if (disabled) return;
194
- if (externalTimer) {
195
- clearTimeout(externalTimer);
196
- externalTimer = undefined;
197
- }
198
- // 轮内工具步继承轮次开始时刻(外部修改步不标 ts,按实际发生时间归属)
199
- queue.push(meta.kind === "tool" && turnTs !== undefined ? { ...meta, ts: meta.ts ?? turnTs } : meta);
200
- if (flushTimer) clearTimeout(flushTimer);
201
- flushTimer = setTimeout(flush, SNAPSHOT_DEBOUNCE_MS);
202
- };
203
-
204
- const onFsChange = (_type: string, filename: string | Buffer | null) => {
205
- if (disabled || disposed) return;
206
- const rel = filename == null ? "" : String(filename).replace(/\\/g, "/");
207
- if (rel && (isExcludedRelPath(rel) || WATCHER_FILE_NOISE.test(rel))) return;
208
- if (running.size > 0) {
209
- if (!rel || pending.size + pendingStats >= MAX_PENDING) return;
210
- pendingStats += 1;
211
- // fs.watch 回调不能同步 stat;目录标记允许异步到达。
212
- void fs.promises.stat(path.join(cwd, rel)).then(
213
- (info) => info.isDirectory(),
214
- () => false,
215
- ).then((isDir) => {
216
- if (disposed || disabled) return;
217
- const livePath = isDir ? `${rel}/` : rel;
218
- if (pending.size >= MAX_PENDING || pending.has(livePath)) return;
219
- pending.add(livePath);
220
- if (!pendingTimer) pendingTimer = setTimeout(publishPending, PENDING_THROTTLE_MS);
221
- }).finally(() => { pendingStats -= 1; });
222
- return;
223
- }
224
- // 工具刚结束、快照还没拍:这次改动归它,不另记外部修改
225
- if (queue.length || flushTimer) return;
226
- if (externalTimer) clearTimeout(externalTimer);
227
- externalTimer = setTimeout(() => {
228
- externalTimer = undefined;
229
- if (running.size > 0 || queue.length) return;
230
- void record({ kind: "external", label: "" });
231
- }, EXTERNAL_DEBOUNCE_MS);
232
- };
233
-
234
- const startWatcher = () => {
235
- if (watcher || watcherFailed || disabled) return;
236
- try {
237
- // Watch the canonical path: on Windows an 8.3 short name (RUNNER~1) can make
238
- // libuv's directory watcher compare mismatched spellings and crash the process.
239
- let watchRoot = cwd;
240
- try {
241
- watchRoot = realpathSync.native(cwd);
242
- } catch {
243
- /* keep the given path when it cannot be resolved */
244
- }
245
- watcher = fs.watch(watchRoot, { recursive: true, persistent: false }, onFsChange);
246
- watcher.on("error", () => {
247
- watcherFailed = true;
248
- try {
249
- watcher?.close();
250
- } catch {
251
- /* ignore */
252
- }
253
- watcher = null;
254
- });
255
- } catch {
256
- watcherFailed = true;
257
- watcher = null;
258
- }
94
+ if (round && !disposed) publish({ type: "growth", round, ts: Date.now() });
95
+ return round;
259
96
  };
260
97
 
261
98
  return {
262
99
  version: GROWTH_TRACKER_VERSION,
263
100
  getError: () => lastErrorMessage,
264
- async prepare() {
265
- // prepare 在首条 prompt 前同步执行:此刻就是轮次开始时刻,基线带上
266
- // (晚落账时轮次窗口仍能对上;基线本身不进轮,但 fromTree 归属要用)
267
- turnTs = Date.now();
268
- startWatcher();
269
- await ensureBaseline();
101
+ prepare() {
102
+ if (disabled || disposed) return chain;
103
+ return enqueue(async () => {
104
+ if (disabled || disposed) return;
105
+ published(await recordWorkspaceChanges(cwd, sessionPath));
106
+ });
270
107
  },
271
108
  onEvent(evt) {
272
109
  if (disposed || disabled) return;
273
110
  switch (evt.type) {
274
111
  case "agent_start":
275
- // 轮次开始时刻:本轮所有步的归属 ts(用户消息时间戳与之对齐,切窗才准)
276
- turnTs = (evt as { ts?: number }).ts ?? Date.now();
277
- startWatcher();
278
- void ensureBaseline();
112
+ // 自动重试会再次 agent_start:一轮只从第一次开始算
113
+ if (runStart === null) runStart = entries().length;
279
114
  break;
280
- case "tool_execution_start": {
281
- if (!isMutatingTool(evt.toolName)) break;
282
- running.add(evt.toolCallId);
283
- if (argsById.size > 64) argsById.delete(argsById.keys().next().value as string);
284
- argsById.set(evt.toolCallId, (evt as { args?: unknown }).args);
285
- if (externalTimer) {
286
- clearTimeout(externalTimer);
287
- externalTimer = undefined;
288
- }
115
+ case "agent_settled": {
116
+ if (runStart === null) break;
117
+ const all = entries();
118
+ const run = all.slice(runStart);
119
+ const { ids, title } = promptsFromEntries(run);
120
+ runStart = null;
121
+ void enqueue(async () => {
122
+ if (disabled || disposed) return;
123
+ published(await commitRound(cwd, { kind: "round", session: sessionPath, title, promptIds: ids, status: roundStatus(run) }));
124
+ });
289
125
  break;
290
126
  }
291
- case "tool_execution_end": {
292
- if (!isMutatingTool(evt.toolName)) break;
293
- running.delete(evt.toolCallId);
294
- const args = argsById.get(evt.toolCallId);
295
- argsById.delete(evt.toolCallId);
296
- schedule({ kind: "tool", label: labelForTool(cwd, evt.toolName, args), toolCallId: evt.toolCallId, toolName: evt.toolName });
297
- break;
298
- }
299
- case "agent_end":
300
- running.clear();
301
- // 先落下本轮最后一批工具快照,再写不依赖文件变化的轮次终点。
302
- if (flushTimer) clearTimeout(flushTimer);
303
- flush();
304
- // 无文件变更的回合也持久化终点,轮数才能与真实用户轮次一致。
305
- // turn 步同样按轮次开始时刻归属,防晚落账切错窗口。
306
- if (!evt.willRetry) void enqueue(() => doRecord({ kind: "turn", label: "", ts: turnTs }, true));
307
- turnTs = undefined;
308
- break;
309
127
  default:
310
128
  break;
311
129
  }
312
130
  },
313
- async snapshotNow(label = "") {
131
+ async recordNow() {
314
132
  if (disabled) throw disabledFailure ?? new GrowthError("growth tracking is disabled for this workspace", "unavailable");
315
- let result: GrowthStep | null = null;
133
+ let result: GrowthRound | null = null;
316
134
  let failure: unknown;
317
135
  await enqueue(async () => {
318
136
  try {
319
137
  if (disabled) throw disabledFailure ?? new GrowthError("growth tracking is disabled for this workspace", "unavailable");
320
- const step = await snapshot(cwd, { kind: "manual", label, session: sessionPath });
321
- if (errorActive) {
322
- errorActive = false;
323
- lastErrorMessage = null;
324
- publish({ type: "growth_error", message: null, ts: Date.now() });
325
- }
326
- if (step) {
327
- result = step;
328
- publish({ type: "growth", step, ts: Date.now() });
329
- }
138
+ result = published(await recordWorkspaceChanges(cwd, sessionPath));
330
139
  } catch (error) {
331
140
  failure = error;
332
141
  reportError(error);
@@ -337,15 +146,6 @@ export function createGrowthTracker(opts: { cwd: string; sessionPath: string; pu
337
146
  },
338
147
  dispose() {
339
148
  disposed = true;
340
- if (flushTimer) clearTimeout(flushTimer);
341
- if (pendingTimer) clearTimeout(pendingTimer);
342
- if (externalTimer) clearTimeout(externalTimer);
343
- try {
344
- watcher?.close();
345
- } catch {
346
- /* ignore */
347
- }
348
- watcher = null;
349
149
  },
350
150
  };
351
151
  }
@@ -7,7 +7,7 @@ interface SessionEntry {
7
7
  type?: string;
8
8
  id?: string;
9
9
  timestamp?: string | number;
10
- message?: { role?: string; timestamp?: number };
10
+ message?: { role?: string; timestamp?: number; content?: unknown };
11
11
  }
12
12
 
13
13
  /** 与会话统计一样遍历全部 JSONL 条目,不使用压缩后的渲染消息列表。 */
@@ -21,3 +21,33 @@ export function userTurnsFromEntries(entries: readonly SessionEntry[]): SessionT
21
21
  }
22
22
  return turns;
23
23
  }
24
+
25
+ export interface RoundPrompts {
26
+ /** 本轮的用户消息 entryId(按出现顺序) */
27
+ ids: string[];
28
+ /** 第一条用户消息的首个非空行(纯图片提问为空) */
29
+ title: string;
30
+ }
31
+
32
+ function textOf(content: unknown): string {
33
+ if (typeof content === "string") return content;
34
+ if (!Array.isArray(content)) return "";
35
+ return content
36
+ .map((part) => (part && typeof part === "object" && (part as { type?: unknown }).type === "text" ? String((part as { text?: unknown }).text ?? "") : ""))
37
+ .join("");
38
+ }
39
+
40
+ /**
41
+ * 一次运行(agent_start → agent_settled)新增的用户消息:首条提问 + steer / followUp 追加的消息。
42
+ * 遍历原始 JSONL 条目而不是渲染消息:上下文压缩后不再渲染的消息也算。
43
+ */
44
+ export function promptsFromEntries(entries: readonly SessionEntry[], fromIndex = 0): RoundPrompts {
45
+ const ids: string[] = [];
46
+ let title = "";
47
+ for (const entry of entries.slice(Math.max(0, fromIndex))) {
48
+ if (entry.type !== "message" || entry.message?.role !== "user" || !entry.id) continue;
49
+ ids.push(entry.id);
50
+ if (!title) title = textOf(entry.message.content).split("\n").map((line) => line.trim()).find(Boolean) ?? "";
51
+ }
52
+ return { ids, title };
53
+ }
@@ -0,0 +1,15 @@
1
+ import { resolveModelScopeWithDiagnostics } from "@earendil-works/pi-coding-agent";
2
+
3
+ export type ModelScopeRuntime = Parameters<typeof resolveModelScopeWithDiagnostics>[1];
4
+
5
+ /** 用 pi 自己的模型通配规则过滤 PiWeb 选择器里的模型 */
6
+ export async function filterModelsByPiScope<T extends { provider: string; id: string }>(
7
+ models: T[],
8
+ patterns: string[],
9
+ runtime: ModelScopeRuntime,
10
+ ): Promise<T[]> {
11
+ if (patterns.length === 0) return models;
12
+ const { scopedModels } = await resolveModelScopeWithDiagnostics(patterns, runtime);
13
+ const allowed = new Set(scopedModels.map(({ model }) => `${model.provider}/${model.id}`));
14
+ return models.filter((model) => allowed.has(`${model.provider}/${model.id}`));
15
+ }
@@ -10,7 +10,9 @@ import { request as httpsRequest } from "node:https";
10
10
  import { BlockList, isIP } from "node:net";
11
11
  import { ModelRuntime } from "@earendil-works/pi-coding-agent";
12
12
  import { getNodeValue, parseTree, printParseErrorCode, type ParseError } from "jsonc-parser";
13
+ import { filterModelsByPiScope } from "./models/model-scope";
13
14
  import { getAgentDir, getModelRuntime, resetModelRuntime } from "./pi";
15
+ import { getEnabledModelPatterns } from "./pi-settings";
14
16
  import { reloadSessionsForCwd } from "./agent-manager";
15
17
  import {
16
18
  getSupportedThinkingLevels, InMemoryCredentialStore, InMemoryModelsStore,
@@ -103,6 +105,20 @@ export function listModels(): Promise<{
103
105
  return value;
104
106
  }
105
107
 
108
+ /** 对话里的模型选择器遵循 pi 的 enabledModels;设置页仍用完整目录 */
109
+ export async function listModelsForPiWeb(): Promise<{ providers: ProviderView[]; models: ModelView[] }> {
110
+ const data = await listModels();
111
+ const patterns = await getEnabledModelPatterns();
112
+ if (!patterns?.length) return data;
113
+ const models = await filterModelsByPiScope(data.models, patterns, await getModelRuntime());
114
+ const counts = new Map<string, number>();
115
+ for (const model of models) counts.set(model.provider, (counts.get(model.provider) ?? 0) + 1);
116
+ return {
117
+ models,
118
+ providers: data.providers.map((provider) => ({ ...provider, modelCount: counts.get(provider.id) ?? 0 })),
119
+ };
120
+ }
121
+
106
122
  /** auth.json 各条目的凭证类型(与 storedCredentialFor 同样的两层数据结构);读取失败不影响目录 */
107
123
  async function readStoredAuthTypes(): Promise<Map<string, "api_key" | "oauth">> {
108
124
  const out = new Map<string, "api_key" | "oauth">();
@@ -43,6 +43,13 @@ async function readSettingsFileStrict(): Promise<Record<string, unknown>> {
43
43
  return parsed as Record<string, unknown>;
44
44
  }
45
45
 
46
+ /** enabledModels 是 pi 的模型范围;没写或写错就不过滤 PiWeb 的选择器 */
47
+ export async function getEnabledModelPatterns(): Promise<string[] | null> {
48
+ const patterns = (await readSettingsFileStrict()).enabledModels;
49
+ if (!Array.isArray(patterns) || patterns.some((pattern) => typeof pattern !== "string")) return null;
50
+ return patterns;
51
+ }
52
+
46
53
  function section(raw: Record<string, unknown>, key: "compaction" | "retry"): Record<string, unknown> {
47
54
  const value = raw[key];
48
55
  return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
package/src/lib/pi.ts CHANGED
@@ -9,7 +9,6 @@ import {
9
9
  type SettingsManager,
10
10
  DefaultPackageManager,
11
11
  DefaultResourceLoader,
12
- type InlineExtension,
13
12
  ModelRuntime,
14
13
  ProjectTrustStore,
15
14
  SessionManager,
@@ -158,10 +157,10 @@ function loaderKey(cwd: string): string {
158
157
  }
159
158
 
160
159
  /** 每个热会话必须拥有自己的扩展 runtime;这里只创建,不加入发现缓存。 */
161
- export async function createSessionResourceLoader(cwd: string, extensionFactories: InlineExtension[] = []): Promise<DefaultResourceLoader> {
160
+ export async function createSessionResourceLoader(cwd: string): Promise<DefaultResourceLoader> {
162
161
  const settingsManager = getSettingsManager(cwd);
163
162
  settingsManager.setProjectTrusted(resolveProjectTrust(cwd).trusted);
164
- const loader = new DefaultResourceLoader({ cwd: path.resolve(cwd), agentDir: getAgentDir(), settingsManager, extensionFactories });
163
+ const loader = new DefaultResourceLoader({ cwd: path.resolve(cwd), agentDir: getAgentDir(), settingsManager });
165
164
  const reload = loader.reload.bind(loader);
166
165
  loader.reload = async (options) => {
167
166
  settingsManager.setProjectTrusted(resolveProjectTrust(cwd).trusted);
@@ -160,7 +160,6 @@ export const claudeSource: ImportSourceModule = {
160
160
  const skipped: string[] = [];
161
161
  let sidechains = 0;
162
162
  let synthetic = 0;
163
- let runtimeContext = 0;
164
163
  let projectPath = summary.projectPath;
165
164
  let model = summary.model;
166
165
  let provider: string | undefined;
@@ -170,12 +169,6 @@ export const claudeSource: ImportSourceModule = {
170
169
  sidechains += 1;
171
170
  continue;
172
171
  }
173
- // Claude 的 attachment/system 是技能列表、token 提醒、环境快照等运行时上下文;
174
- // pi 会按目标工作区生成自己的版本,不能伪装成真人 user 消息,但要在报告中明确告知。
175
- if (record.type === "attachment" || record.type === "system") {
176
- runtimeContext += 1;
177
- continue;
178
- }
179
172
  if (typeof record.cwd === "string" && record.cwd.trim()) projectPath = projectPath ?? record.cwd;
180
173
  const message = record.message as Record<string, unknown> | undefined;
181
174
  if (!message) continue;
@@ -213,12 +206,9 @@ export const claudeSource: ImportSourceModule = {
213
206
  if (record.type === "user") {
214
207
  const { text: textBlocks, results } = splitUserContent((message as { content?: unknown }).content);
215
208
  const plain = textBlocks.filter((block) => block.type === "text").map((block) => block.text).join("\n");
216
- const nonText = textBlocks.filter((block) => block.type !== "text");
209
+ const keepText = textBlocks.filter((block) => block.type !== "text");
217
210
  if (plain && !isSynthetic(plain.trim()) && !/^Caveat:/i.test(plain.trim())) {
218
- entries.push({ type: "message", message: { role: "user", content: [...nonText, { type: "text", text: plain }], timestamp } as ImportedMessage });
219
- } else if (!plain && nonText.length) {
220
- // 图片可以是用户消息的全部内容,不能因为没有文字就把整轮丢掉
221
- entries.push({ type: "message", message: { role: "user", content: nonText, timestamp } as ImportedMessage });
211
+ entries.push({ type: "message", message: { role: "user", content: [...keepText, { type: "text", text: plain }], timestamp } as ImportedMessage });
222
212
  } else if (plain) {
223
213
  synthetic += 1;
224
214
  }
@@ -233,7 +223,6 @@ export const claudeSource: ImportSourceModule = {
233
223
 
234
224
  if (sidechains) skipped.push(`跳过 ${sidechains} 条侧链(子代理)记录`);
235
225
  if (synthetic) skipped.push(`跳过 ${synthetic} 条合成输入(命令回显/系统提醒)`);
236
- if (runtimeContext) skipped.push(`跳过 ${runtimeContext} 条源工具运行时上下文(pi 会按当前工作区重新生成)`);
237
226
  if (!entries.some((entry) => entry.type === "message")) return null;
238
227
  return { summary: { ...summary, projectPath, model, provider }, entries, skipped };
239
228
  },