@baique/pi-web-sky 0.1.35 → 0.1.37

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 (483) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +11 -11
  3. package/.next/build-manifest.json +4 -4
  4. package/.next/next-server.js.nft.json +1 -1
  5. package/.next/prerender-manifest.json +3 -3
  6. package/.next/react-loadable-manifest.json +2 -2
  7. package/.next/required-server-files.js +6 -6
  8. package/.next/required-server-files.json +6 -6
  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 +3 -3
  20. package/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  21. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +3 -3
  22. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  23. package/.next/server/app/api/agent/[id]/bash-output/route.js +2 -2
  24. package/.next/server/app/api/agent/[id]/bash-output/route_client-reference-manifest.js +1 -1
  25. package/.next/server/app/api/agent/[id]/events/route.js +3 -3
  26. package/.next/server/app/api/agent/[id]/events/route_client-reference-manifest.js +1 -1
  27. package/.next/server/app/api/agent/[id]/route.js +2 -2
  28. package/.next/server/app/api/agent/[id]/route_client-reference-manifest.js +1 -1
  29. package/.next/server/app/api/agent/new/route.js +1 -1
  30. package/.next/server/app/api/agent/new/route_client-reference-manifest.js +1 -1
  31. package/.next/server/app/api/agent/running/route.js +1 -1
  32. package/.next/server/app/api/agent/running/route_client-reference-manifest.js +1 -1
  33. package/.next/server/app/api/app-update/route.js +1 -1
  34. package/.next/server/app/api/app-update/route_client-reference-manifest.js +1 -1
  35. package/.next/server/app/api/auth/all-providers/route.js +1 -1
  36. package/.next/server/app/api/auth/all-providers/route_client-reference-manifest.js +1 -1
  37. package/.next/server/app/api/auth/api-key/[provider]/route.js +1 -1
  38. package/.next/server/app/api/auth/api-key/[provider]/route_client-reference-manifest.js +1 -1
  39. package/.next/server/app/api/auth/login/[provider]/route.js +3 -3
  40. package/.next/server/app/api/auth/login/[provider]/route_client-reference-manifest.js +1 -1
  41. package/.next/server/app/api/auth/logout/[provider]/route.js +1 -1
  42. package/.next/server/app/api/auth/logout/[provider]/route_client-reference-manifest.js +1 -1
  43. package/.next/server/app/api/auth/providers/route.js +1 -1
  44. package/.next/server/app/api/auth/providers/route_client-reference-manifest.js +1 -1
  45. package/.next/server/app/api/boards/[id]/canvas/route.js +1 -1
  46. package/.next/server/app/api/boards/[id]/canvas/route_client-reference-manifest.js +1 -1
  47. package/.next/server/app/api/boards/[id]/edges/[eid]/route.js +1 -1
  48. package/.next/server/app/api/boards/[id]/edges/[eid]/route_client-reference-manifest.js +1 -1
  49. package/.next/server/app/api/boards/[id]/edges/route.js +1 -1
  50. package/.next/server/app/api/boards/[id]/edges/route_client-reference-manifest.js +1 -1
  51. package/.next/server/app/api/boards/[id]/nodes/[nid]/route.js +1 -1
  52. package/.next/server/app/api/boards/[id]/nodes/[nid]/route_client-reference-manifest.js +1 -1
  53. package/.next/server/app/api/boards/[id]/nodes/route.js +1 -1
  54. package/.next/server/app/api/boards/[id]/nodes/route_client-reference-manifest.js +1 -1
  55. package/.next/server/app/api/boards/[id]/reconcile-task-edges/route.js +1 -1
  56. package/.next/server/app/api/boards/[id]/reconcile-task-edges/route_client-reference-manifest.js +1 -1
  57. package/.next/server/app/api/boards/[id]/route.js +1 -1
  58. package/.next/server/app/api/boards/[id]/route_client-reference-manifest.js +1 -1
  59. package/.next/server/app/api/boards/purge-orphans/route.js +1 -1
  60. package/.next/server/app/api/boards/purge-orphans/route_client-reference-manifest.js +1 -1
  61. package/.next/server/app/api/boards/reorder/route.js +1 -1
  62. package/.next/server/app/api/boards/reorder/route_client-reference-manifest.js +1 -1
  63. package/.next/server/app/api/boards/route.js +1 -1
  64. package/.next/server/app/api/boards/route_client-reference-manifest.js +1 -1
  65. package/.next/server/app/api/cwd/browse/route.js +1 -1
  66. package/.next/server/app/api/cwd/browse/route_client-reference-manifest.js +1 -1
  67. package/.next/server/app/api/cwd/validate/route.js +2 -2
  68. package/.next/server/app/api/cwd/validate/route_client-reference-manifest.js +1 -1
  69. package/.next/server/app/api/default-cwd/route.js +2 -2
  70. package/.next/server/app/api/default-cwd/route_client-reference-manifest.js +1 -1
  71. package/.next/server/app/api/file-index/route.js +2 -2
  72. package/.next/server/app/api/file-index/route_client-reference-manifest.js +1 -1
  73. package/.next/server/app/api/files/[...path]/route.js +2 -2
  74. package/.next/server/app/api/files/[...path]/route_client-reference-manifest.js +1 -1
  75. package/.next/server/app/api/git/diff/route.js +1 -1
  76. package/.next/server/app/api/git/diff/route_client-reference-manifest.js +1 -1
  77. package/.next/server/app/api/git/status/route.js +1 -1
  78. package/.next/server/app/api/git/status/route_client-reference-manifest.js +1 -1
  79. package/.next/server/app/api/home/route.js +1 -1
  80. package/.next/server/app/api/home/route_client-reference-manifest.js +1 -1
  81. package/.next/server/app/api/mcp/route.js +2 -2
  82. package/.next/server/app/api/mcp/route_client-reference-manifest.js +1 -1
  83. package/.next/server/app/api/models/route.js +2 -2
  84. package/.next/server/app/api/models/route_client-reference-manifest.js +1 -1
  85. package/.next/server/app/api/models-config/catalog/route.js +1 -1
  86. package/.next/server/app/api/models-config/catalog/route_client-reference-manifest.js +1 -1
  87. package/.next/server/app/api/models-config/discover/route.js +1 -1
  88. package/.next/server/app/api/models-config/discover/route_client-reference-manifest.js +1 -1
  89. package/.next/server/app/api/models-config/route.js +1 -1
  90. package/.next/server/app/api/models-config/route_client-reference-manifest.js +1 -1
  91. package/.next/server/app/api/models-config/test/route.js +1 -1
  92. package/.next/server/app/api/models-config/test/route_client-reference-manifest.js +1 -1
  93. package/.next/server/app/api/plugins/route.js +2 -2
  94. package/.next/server/app/api/plugins/route_client-reference-manifest.js +1 -1
  95. package/.next/server/app/api/project-trust/route.js +2 -2
  96. package/.next/server/app/api/project-trust/route_client-reference-manifest.js +1 -1
  97. package/.next/server/app/api/quota/route.js +1 -1
  98. package/.next/server/app/api/quota/route_client-reference-manifest.js +1 -1
  99. package/.next/server/app/api/search/route.js +2 -2
  100. package/.next/server/app/api/search/route_client-reference-manifest.js +1 -1
  101. package/.next/server/app/api/sessions/[id]/auto-name/route.js +3 -3
  102. package/.next/server/app/api/sessions/[id]/auto-name/route_client-reference-manifest.js +1 -1
  103. package/.next/server/app/api/sessions/[id]/context/route.js +2 -2
  104. package/.next/server/app/api/sessions/[id]/context/route_client-reference-manifest.js +1 -1
  105. package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route.js +2 -2
  106. package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route_client-reference-manifest.js +1 -1
  107. package/.next/server/app/api/sessions/[id]/export/route.js +2 -2
  108. package/.next/server/app/api/sessions/[id]/export/route_client-reference-manifest.js +1 -1
  109. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  110. package/.next/server/app/api/sessions/[id]/route_client-reference-manifest.js +1 -1
  111. package/.next/server/app/api/sessions/[id]/state/route.js +2 -2
  112. package/.next/server/app/api/sessions/[id]/state/route_client-reference-manifest.js +1 -1
  113. package/.next/server/app/api/sessions/[id]/todos/route.js +2 -2
  114. package/.next/server/app/api/sessions/[id]/todos/route_client-reference-manifest.js +1 -1
  115. package/.next/server/app/api/sessions/route.js +2 -2
  116. package/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -1
  117. package/.next/server/app/api/skills/check/route.js +1 -1
  118. package/.next/server/app/api/skills/check/route.js.nft.json +1 -1
  119. package/.next/server/app/api/skills/check/route_client-reference-manifest.js +1 -1
  120. package/.next/server/app/api/skills/install/route.js +2 -2
  121. package/.next/server/app/api/skills/install/route_client-reference-manifest.js +1 -1
  122. package/.next/server/app/api/skills/route.js +2 -31
  123. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  124. package/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
  125. package/.next/server/app/api/skills/search/route.js +1 -1
  126. package/.next/server/app/api/skills/search/route_client-reference-manifest.js +1 -1
  127. package/.next/server/app/api/skills/update/route.js +1 -1
  128. package/.next/server/app/api/skills/update/route.js.nft.json +1 -1
  129. package/.next/server/app/api/skills/update/route_client-reference-manifest.js +1 -1
  130. package/.next/server/app/api/task-card-questions/[id]/answer/route.js +1 -1
  131. package/.next/server/app/api/task-card-questions/[id]/answer/route_client-reference-manifest.js +1 -1
  132. package/.next/server/app/api/task-card-questions/route.js +1 -1
  133. package/.next/server/app/api/task-card-questions/route_client-reference-manifest.js +1 -1
  134. package/.next/server/app/api/task-cards/[id]/route.js +1 -1
  135. package/.next/server/app/api/task-cards/[id]/route_client-reference-manifest.js +1 -1
  136. package/.next/server/app/api/task-cards/route.js +1 -1
  137. package/.next/server/app/api/task-cards/route_client-reference-manifest.js +1 -1
  138. package/.next/server/app/api/task-scheduler/status/route.js +1 -1
  139. package/.next/server/app/api/task-scheduler/status/route_client-reference-manifest.js +1 -1
  140. package/.next/server/app/api/tasks/[id]/board/route.js +1 -1
  141. package/.next/server/app/api/tasks/[id]/board/route_client-reference-manifest.js +1 -1
  142. package/.next/server/app/api/tasks/[id]/route.js +1 -1
  143. package/.next/server/app/api/tasks/[id]/route_client-reference-manifest.js +1 -1
  144. package/.next/server/app/api/tasks/reorder/route.js +1 -1
  145. package/.next/server/app/api/tasks/reorder/route_client-reference-manifest.js +1 -1
  146. package/.next/server/app/api/tasks/route.js +1 -1
  147. package/.next/server/app/api/tasks/route_client-reference-manifest.js +1 -1
  148. package/.next/server/app/api/terminal/[id]/events/route.js +3 -3
  149. package/.next/server/app/api/terminal/[id]/events/route_client-reference-manifest.js +1 -1
  150. package/.next/server/app/api/terminal/[id]/route.js +2 -2
  151. package/.next/server/app/api/terminal/[id]/route_client-reference-manifest.js +1 -1
  152. package/.next/server/app/api/terminal/route.js +2 -2
  153. package/.next/server/app/api/terminal/route_client-reference-manifest.js +1 -1
  154. package/.next/server/app/api/worktrees/route.js +2 -2
  155. package/.next/server/app/api/worktrees/route_client-reference-manifest.js +1 -1
  156. package/.next/server/app/favicon.ico/route.js +1 -1
  157. package/.next/server/app/index.html +1 -1
  158. package/.next/server/app/index.rsc +5 -5
  159. package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
  160. package/.next/server/app/index.segments/_full.segment.rsc +5 -5
  161. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  162. package/.next/server/app/manifest.webmanifest/route.js +2 -2
  163. package/.next/server/app/manifest.webmanifest/route_client-reference-manifest.js +1 -1
  164. package/.next/server/app/page.js +3 -3
  165. package/.next/server/app/page_client-reference-manifest.js +1 -1
  166. package/.next/server/app-paths-manifest.json +11 -11
  167. package/.next/server/chunks/2404.js +2 -2
  168. package/.next/server/chunks/2430.js +4 -4
  169. package/.next/server/chunks/3500.js +30 -0
  170. package/.next/server/chunks/4226.js +2 -2
  171. package/.next/server/chunks/542.js +11 -6
  172. package/.next/server/chunks/5794.js +2 -2
  173. package/.next/server/chunks/6429.js +1 -1
  174. package/.next/server/chunks/898.js +2 -2
  175. package/.next/server/instrumentation.js +1 -1
  176. package/.next/server/middleware-build-manifest.js +1 -1
  177. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  178. package/.next/server/middleware.js +3 -3
  179. package/.next/server/next-font-manifest.js +1 -1
  180. package/.next/server/next-font-manifest.json +1 -1
  181. package/.next/server/pages/404.html +1 -1
  182. package/.next/server/pages/500.html +1 -1
  183. package/.next/server/server-reference-manifest.json +1 -1
  184. package/.next/static/chunks/5315.dba7515ae6e139ae.js +4 -0
  185. package/.next/static/chunks/{7999.f4ab221861378788.js → 7999.879d5a20e8641752.js} +1 -1
  186. package/.next/static/chunks/app/_not-found/{page-af4fe23994cb3d66.js → page-8cea8d8de35f4da4.js} +1 -1
  187. package/.next/static/chunks/app/{layout-1d9a7a44fbc92d2e.js → layout-c4ca28ccc08bc975.js} +1 -1
  188. package/.next/static/chunks/app/{page-6d6779a81a0a7639.js → page-c9ead38525a85686.js} +3 -3
  189. package/.next/static/chunks/{main-app-b3e8a8d15d83e1f0.js → main-app-dd2385a1a5a588df.js} +1 -1
  190. package/.next/static/chunks/next/dist/client/components/builtin/global-error-be943dcb89867ab9.js +1 -0
  191. package/.next/static/chunks/{webpack-fa4f40085b1ec1ac.js → webpack-4bbf04891e910713.js} +1 -1
  192. package/.next/static/css/422c7dc0faa04d89.css +1 -0
  193. package/.next/trace +8 -9
  194. package/.next/trace-build +1 -1
  195. package/.next/types/app/api/agent/[id]/bash-output/route.ts +1 -1
  196. package/.next/types/app/api/agent/[id]/events/route.ts +1 -1
  197. package/.next/types/app/api/agent/[id]/route.ts +1 -1
  198. package/.next/types/app/api/agent/new/route.ts +1 -1
  199. package/.next/types/app/api/agent/running/route.ts +1 -1
  200. package/.next/types/app/api/app-update/route.ts +1 -1
  201. package/.next/types/app/api/auth/all-providers/route.ts +1 -1
  202. package/.next/types/app/api/auth/api-key/[provider]/route.ts +1 -1
  203. package/.next/types/app/api/auth/login/[provider]/route.ts +1 -1
  204. package/.next/types/app/api/auth/logout/[provider]/route.ts +1 -1
  205. package/.next/types/app/api/auth/providers/route.ts +1 -1
  206. package/.next/types/app/api/boards/[id]/canvas/route.ts +1 -1
  207. package/.next/types/app/api/boards/[id]/edges/[eid]/route.ts +1 -1
  208. package/.next/types/app/api/boards/[id]/edges/route.ts +1 -1
  209. package/.next/types/app/api/boards/[id]/nodes/[nid]/route.ts +1 -1
  210. package/.next/types/app/api/boards/[id]/nodes/route.ts +1 -1
  211. package/.next/types/app/api/boards/[id]/reconcile-task-edges/route.ts +1 -1
  212. package/.next/types/app/api/boards/[id]/route.ts +1 -1
  213. package/.next/types/app/api/boards/purge-orphans/route.ts +1 -1
  214. package/.next/types/app/api/boards/reorder/route.ts +1 -1
  215. package/.next/types/app/api/boards/route.ts +1 -1
  216. package/.next/types/app/api/cwd/browse/route.ts +1 -1
  217. package/.next/types/app/api/cwd/validate/route.ts +1 -1
  218. package/.next/types/app/api/default-cwd/route.ts +1 -1
  219. package/.next/types/app/api/file-index/route.ts +1 -1
  220. package/.next/types/app/api/files/[...path]/route.ts +1 -1
  221. package/.next/types/app/api/git/diff/route.ts +1 -1
  222. package/.next/types/app/api/git/status/route.ts +1 -1
  223. package/.next/types/app/api/home/route.ts +1 -1
  224. package/.next/types/app/api/mcp/route.ts +1 -1
  225. package/.next/types/app/api/models/route.ts +1 -1
  226. package/.next/types/app/api/models-config/catalog/route.ts +1 -1
  227. package/.next/types/app/api/models-config/discover/route.ts +1 -1
  228. package/.next/types/app/api/models-config/route.ts +1 -1
  229. package/.next/types/app/api/models-config/test/route.ts +1 -1
  230. package/.next/types/app/api/plugins/route.ts +1 -1
  231. package/.next/types/app/api/project-trust/route.ts +1 -1
  232. package/.next/types/app/api/quota/route.ts +1 -1
  233. package/.next/types/app/api/search/route.ts +1 -1
  234. package/.next/types/app/api/sessions/[id]/auto-name/route.ts +1 -1
  235. package/.next/types/app/api/sessions/[id]/context/route.ts +1 -1
  236. package/.next/types/app/api/sessions/[id]/entries/[entryId]/thinking/route.ts +1 -1
  237. package/.next/types/app/api/sessions/[id]/export/route.ts +1 -1
  238. package/.next/types/app/api/sessions/[id]/route.ts +1 -1
  239. package/.next/types/app/api/sessions/[id]/state/route.ts +1 -1
  240. package/.next/types/app/api/sessions/[id]/todos/route.ts +1 -1
  241. package/.next/types/app/api/sessions/route.ts +1 -1
  242. package/.next/types/app/api/skills/check/route.ts +1 -1
  243. package/.next/types/app/api/skills/install/route.ts +1 -1
  244. package/.next/types/app/api/skills/route.ts +1 -1
  245. package/.next/types/app/api/skills/search/route.ts +1 -1
  246. package/.next/types/app/api/skills/update/route.ts +1 -1
  247. package/.next/types/app/api/task-card-questions/[id]/answer/route.ts +1 -1
  248. package/.next/types/app/api/task-card-questions/route.ts +1 -1
  249. package/.next/types/app/api/task-cards/[id]/route.ts +1 -1
  250. package/.next/types/app/api/task-cards/route.ts +1 -1
  251. package/.next/types/app/api/task-scheduler/status/route.ts +1 -1
  252. package/.next/types/app/api/tasks/[id]/board/route.ts +1 -1
  253. package/.next/types/app/api/tasks/[id]/route.ts +1 -1
  254. package/.next/types/app/api/tasks/reorder/route.ts +1 -1
  255. package/.next/types/app/api/tasks/route.ts +1 -1
  256. package/.next/types/app/api/terminal/[id]/events/route.ts +1 -1
  257. package/.next/types/app/api/terminal/[id]/route.ts +1 -1
  258. package/.next/types/app/api/terminal/route.ts +1 -1
  259. package/.next/types/app/api/worktrees/route.ts +1 -1
  260. package/.next/types/app/layout.ts +1 -1
  261. package/.next/types/app/page.ts +1 -1
  262. package/bin/pi-web-sky.js +15 -20
  263. package/lib/agent-client.test.mjs +64 -0
  264. package/lib/agent-client.ts +53 -0
  265. package/lib/agent-event-connection.test.mjs +193 -0
  266. package/lib/agent-event-connection.ts +202 -0
  267. package/lib/agent-event-stream.test.mjs +155 -0
  268. package/lib/agent-event-stream.ts +132 -0
  269. package/lib/agent-event-wire.test.mjs +278 -0
  270. package/lib/agent-event-wire.ts +107 -0
  271. package/lib/agent-phase.ts +24 -0
  272. package/lib/allowed-roots.ts +31 -0
  273. package/lib/ansi.test.mjs +52 -0
  274. package/lib/ansi.ts +211 -0
  275. package/lib/api-types.ts +139 -0
  276. package/lib/app-update.test.mjs +27 -0
  277. package/lib/app-update.ts +28 -0
  278. package/lib/atomic-file.test.mjs +37 -0
  279. package/lib/atomic-file.ts +34 -0
  280. package/lib/audit-session.ts +235 -0
  281. package/lib/bash-output.test.mjs +63 -0
  282. package/lib/bash-output.ts +53 -0
  283. package/lib/bg-image.ts +129 -0
  284. package/lib/board-events.ts +83 -0
  285. package/lib/board-purge.test.mjs +66 -0
  286. package/lib/board-purge.ts +65 -0
  287. package/lib/board-search.test.mjs +110 -0
  288. package/lib/board-search.ts +63 -0
  289. package/lib/board-store.test.mjs +542 -0
  290. package/lib/board-store.ts +794 -0
  291. package/lib/board-types.ts +96 -0
  292. package/lib/board-utils.ts +17 -0
  293. package/lib/bounded-form-data.test.mjs +54 -0
  294. package/lib/bounded-form-data.ts +49 -0
  295. package/lib/browser-notifications.test.mjs +167 -0
  296. package/lib/browser-notifications.ts +104 -0
  297. package/lib/chat-lazy-load.test.mjs +140 -0
  298. package/lib/chat-lazy-load.ts +64 -0
  299. package/lib/clipboard-paths.test.mjs +140 -0
  300. package/lib/clipboard-paths.ts +192 -0
  301. package/lib/clipboard.ts +18 -0
  302. package/lib/compaction-summary.test.mjs +44 -0
  303. package/lib/compaction-summary.ts +34 -0
  304. package/lib/custom-ui-terminal.test.mjs +32 -0
  305. package/lib/custom-ui-terminal.ts +27 -0
  306. package/lib/deepseek-pricing.test.mjs +120 -0
  307. package/lib/deepseek-pricing.ts +71 -0
  308. package/lib/directory-browser.test.mjs +60 -0
  309. package/lib/directory-browser.ts +83 -0
  310. package/lib/draft-stash.test.mjs +91 -0
  311. package/lib/draft-stash.ts +102 -0
  312. package/lib/draft-store.ts +105 -0
  313. package/lib/dropdown-direction.ts +27 -0
  314. package/lib/extension-command.test.mjs +63 -0
  315. package/lib/extension-command.ts +81 -0
  316. package/lib/file-access.test.mjs +31 -0
  317. package/lib/file-access.ts +59 -0
  318. package/lib/file-dirent.test.mjs +56 -0
  319. package/lib/file-dirent.ts +17 -0
  320. package/lib/file-explorer-state.test.mjs +63 -0
  321. package/lib/file-explorer-state.ts +36 -0
  322. package/lib/file-fuzzy.test.mjs +29 -0
  323. package/lib/file-fuzzy.ts +189 -0
  324. package/lib/file-links.test.mjs +92 -0
  325. package/lib/file-links.ts +148 -0
  326. package/lib/file-paths.ts +43 -0
  327. package/lib/file-types.test.mjs +34 -0
  328. package/lib/file-types.ts +73 -0
  329. package/lib/file-upload.test.mjs +70 -0
  330. package/lib/file-upload.ts +59 -0
  331. package/lib/file-viewer-state.test.mjs +20 -0
  332. package/lib/file-viewer-state.ts +15 -0
  333. package/lib/frontmatter.test.mjs +55 -0
  334. package/lib/frontmatter.ts +87 -0
  335. package/lib/git-changes.test.mjs +52 -0
  336. package/lib/git-changes.ts +232 -0
  337. package/lib/git-status.ts +47 -0
  338. package/lib/git-types.ts +29 -0
  339. package/lib/http-dispatcher.test.mjs +83 -0
  340. package/lib/http-dispatcher.ts +86 -0
  341. package/lib/i18n/format.test.mjs +19 -0
  342. package/lib/i18n/format.ts +60 -0
  343. package/lib/i18n/messages/en.ts +662 -0
  344. package/lib/i18n/messages/zh-CN.ts +661 -0
  345. package/lib/i18n/registry.test.mjs +32 -0
  346. package/lib/i18n/registry.ts +43 -0
  347. package/lib/i18n/types.ts +15 -0
  348. package/lib/image-attachments.test.mjs +27 -0
  349. package/lib/image-attachments.ts +56 -0
  350. package/lib/initial-navigation.test.mjs +65 -0
  351. package/lib/initial-navigation.ts +15 -0
  352. package/lib/markdown.test.mjs +104 -0
  353. package/lib/markdown.ts +347 -0
  354. package/lib/message-display.test.mjs +130 -0
  355. package/lib/message-display.ts +47 -0
  356. package/lib/model-catalog.test.mjs +327 -0
  357. package/lib/model-catalog.ts +414 -0
  358. package/lib/model-discovery-auth.ts +58 -0
  359. package/lib/model-discovery.test.mjs +50 -0
  360. package/lib/model-discovery.ts +75 -0
  361. package/lib/model-id-preference.ts +46 -0
  362. package/lib/model-scope.test.mjs +193 -0
  363. package/lib/model-scope.ts +198 -0
  364. package/lib/models-cache.test.mjs +122 -0
  365. package/lib/models-cache.ts +90 -0
  366. package/lib/models-config-store.test.mjs +200 -0
  367. package/lib/models-config-store.ts +84 -0
  368. package/lib/next-config-esm.test.mjs +14 -0
  369. package/lib/next-config.test.mjs +13 -0
  370. package/lib/node-version.test.mjs +33 -0
  371. package/lib/normalize.test.mjs +54 -0
  372. package/lib/normalize.ts +63 -0
  373. package/lib/npx.ts +62 -0
  374. package/lib/panel-layout.test.mjs +56 -0
  375. package/lib/panel-layout.ts +52 -0
  376. package/lib/patch.test.mjs +110 -0
  377. package/lib/patch.ts +134 -0
  378. package/lib/path-security.ts +42 -0
  379. package/lib/paths.test.mjs +58 -0
  380. package/lib/paths.ts +71 -0
  381. package/lib/pi-types.ts +183 -0
  382. package/lib/pi-web-options.test.mjs +63 -0
  383. package/lib/process-lifecycle.test.mjs +83 -0
  384. package/lib/project-command-env.test.mjs +298 -0
  385. package/lib/project-command-env.ts +129 -0
  386. package/lib/project-groups.test.mjs +60 -0
  387. package/lib/project-groups.ts +53 -0
  388. package/lib/project-identity.test.mjs +37 -0
  389. package/lib/project-identity.ts +26 -0
  390. package/lib/project-tree.test.mjs +115 -0
  391. package/lib/project-tree.ts +176 -0
  392. package/lib/project-trust.test.mjs +117 -0
  393. package/lib/project-trust.ts +48 -0
  394. package/lib/prompt-recovery.test.mjs +33 -0
  395. package/lib/prompt-recovery.ts +47 -0
  396. package/lib/provider-api-key-route.test.mjs +12 -0
  397. package/lib/provider-credential-store.test.mjs +126 -0
  398. package/lib/provider-credential-store.ts +114 -0
  399. package/lib/provider-listing-runtime.ts +37 -0
  400. package/lib/provider-listing.test.mjs +114 -0
  401. package/lib/provider-listing.ts +118 -0
  402. package/lib/provider-services.ts +64 -0
  403. package/lib/request-security.test.mjs +256 -0
  404. package/lib/request-security.ts +136 -0
  405. package/lib/rpc-manager-shutdown.test.mjs +372 -0
  406. package/lib/rpc-manager-widgets.test.mjs +342 -0
  407. package/lib/rpc-manager.test.mjs +139 -0
  408. package/lib/rpc-manager.ts +1703 -0
  409. package/lib/rpc-session-info.test.mjs +93 -0
  410. package/lib/session-delete.test.mjs +53 -0
  411. package/lib/session-delete.ts +86 -0
  412. package/lib/session-file-references-core.ts +82 -0
  413. package/lib/session-file-references.test.mjs +91 -0
  414. package/lib/session-file-references.ts +29 -0
  415. package/lib/session-path.test.mjs +24 -0
  416. package/lib/session-path.ts +11 -0
  417. package/lib/session-reader.pagination.test.mjs +115 -0
  418. package/lib/session-reader.test.mjs +446 -0
  419. package/lib/session-reader.ts +548 -0
  420. package/lib/session-resolve.test.mjs +65 -0
  421. package/lib/session-row-context-menu.test.mjs +46 -0
  422. package/lib/session-row-context-menu.ts +29 -0
  423. package/lib/session-scanner.test.mjs +151 -0
  424. package/lib/session-scanner.ts +238 -0
  425. package/lib/session-search.test.mjs +173 -0
  426. package/lib/session-search.ts +284 -0
  427. package/lib/session-stats.ts +26 -0
  428. package/lib/session-timing.test.mjs +107 -0
  429. package/lib/session-timing.ts +45 -0
  430. package/lib/session-title.test.mjs +284 -0
  431. package/lib/session-title.ts +255 -0
  432. package/lib/skill-frontmatter.test.mjs +75 -0
  433. package/lib/skill-frontmatter.ts +40 -0
  434. package/lib/skill-lock.test.mjs +182 -0
  435. package/lib/skill-lock.ts +146 -0
  436. package/lib/skill-updates.test.mjs +182 -0
  437. package/lib/skill-updates.ts +263 -0
  438. package/lib/skills-service.ts +16 -0
  439. package/lib/slash-display.test.mjs +122 -0
  440. package/lib/slash-display.ts +30 -0
  441. package/lib/sqlite-db.test.mjs +140 -0
  442. package/lib/sqlite-db.ts +205 -0
  443. package/lib/startup-preferences.test.mjs +130 -0
  444. package/lib/startup-preferences.ts +65 -0
  445. package/lib/streaming-message.test.mjs +212 -0
  446. package/lib/streaming-message.ts +151 -0
  447. package/lib/subagent-widget.test.mjs +87 -0
  448. package/lib/subagent-widget.ts +109 -0
  449. package/lib/sync-room-server.mjs +137 -0
  450. package/lib/task-card-store.test.mjs +309 -0
  451. package/lib/task-card-store.ts +559 -0
  452. package/lib/task-scheduler.ts +498 -0
  453. package/lib/task-store.test.mjs +216 -0
  454. package/lib/task-store.ts +271 -0
  455. package/lib/terminal-input.test.mjs +46 -0
  456. package/lib/terminal-input.ts +63 -0
  457. package/lib/terminal-manager.test.mjs +71 -0
  458. package/lib/terminal-manager.ts +245 -0
  459. package/lib/tool-execution-progress.test.mjs +32 -0
  460. package/lib/tool-execution-progress.ts +29 -0
  461. package/lib/tool-names.ts +25 -0
  462. package/lib/tool-preset-preference.test.mjs +72 -0
  463. package/lib/tool-preset-preference.ts +41 -0
  464. package/lib/tool-presets.test.mjs +56 -0
  465. package/lib/tool-presets.ts +42 -0
  466. package/lib/turn-written-files.test.mjs +137 -0
  467. package/lib/turn-written-files.ts +60 -0
  468. package/lib/types.ts +349 -0
  469. package/lib/wallpaper-settings.ts +191 -0
  470. package/lib/web-auth.test.mjs +50 -0
  471. package/lib/web-auth.ts +45 -0
  472. package/lib/workspace-memory.test.mjs +129 -0
  473. package/lib/workspace-memory.ts +98 -0
  474. package/lib/worktree.test.mjs +50 -0
  475. package/lib/worktree.ts +250 -0
  476. package/package.json +3 -1
  477. package/server.mjs +34 -0
  478. package/.next/server/chunks/7513.js +0 -30
  479. package/.next/static/chunks/5315.03de3c6abd8b283e.js +0 -4
  480. package/.next/static/chunks/next/dist/client/components/builtin/global-error-00a4badaccb388e6.js +0 -1
  481. package/.next/static/css/cf25164cb7c04d95.css +0 -1
  482. /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → 7sDSz-wW-RWYnxh1l6-fD}/_buildManifest.js +0 -0
  483. /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → 7sDSz-wW-RWYnxh1l6-fD}/_ssgManifest.js +0 -0
@@ -0,0 +1,151 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+ import { createJiti } from "jiti";
7
+
8
+ const jiti = createJiti(import.meta.url);
9
+ const { scanSessionFiles } = await jiti.import("./session-scanner.ts");
10
+
11
+ function makeRoot() {
12
+ const root = mkdtempSync(join(tmpdir(), "pi-scan-"));
13
+ const projDir = join(root, "--home-wa-project-demo--");
14
+ mkdirSync(projDir, { recursive: true });
15
+ return { root, projDir };
16
+ }
17
+
18
+ test("scanSessionFiles:读取 header + 首条用户消息,忽略正文", async () => {
19
+ const { root, projDir } = makeRoot();
20
+ try {
21
+ const file = join(projDir, "2026-01-01T00-00-00-000Z_aaa.jsonl");
22
+ writeFileSync(file, [
23
+ '{"type":"session","version":3,"id":"aaa","timestamp":"2026-01-01T00:00:00.000Z","cwd":"/home/wa/project/demo","parentSession":"/x/bbb.jsonl"}',
24
+ '{"type":"model_change","id":"m1","parentId":null,"provider":"p","modelId":"m","timestamp":"2026-01-01T00:00:01.000Z"}',
25
+ '{"type":"message","id":"u1","parentId":null,"message":{"role":"user","content":"你好,请帮我看看这个项目"}}',
26
+ '{"type":"message","id":"a1","parentId":"u1","message":{"role":"assistant","content":[{"type":"text","text":"好的,我看看"}]}}',
27
+ '{"type":"message","id":"u2","parentId":"a1","message":{"role":"user","content":"再帮我优化一下"}}',
28
+ ].join("\n") + "\n");
29
+
30
+ const result = await scanSessionFiles(root);
31
+ assert.equal(result.length, 1);
32
+ const s = result[0];
33
+ assert.equal(s.id, "aaa");
34
+ assert.equal(s.cwd, "/home/wa/project/demo");
35
+ assert.equal(s.firstMessage, "你好,请帮我看看这个项目");
36
+ assert.equal(s.parentSessionPath, "/x/bbb.jsonl");
37
+ assert.equal(s.created.toISOString(), "2026-01-01T00:00:00.000Z");
38
+ assert.ok(s.modified.getTime() > 0);
39
+ assert.equal(typeof s.name, "undefined");
40
+ } finally {
41
+ rmSync(root, { recursive: true, force: true });
42
+ }
43
+ });
44
+
45
+ test("scanSessionFiles:扫描读取最后一个 session_info 的自定义名", async () => {
46
+ const { root, projDir } = makeRoot();
47
+ try {
48
+ const file = join(projDir, "2026-01-01T00-00-00-000Z_bbb.jsonl");
49
+ writeFileSync(file, [
50
+ '{"type":"session","version":3,"id":"bbb","timestamp":"2026-01-01T00:00:00.000Z","cwd":"/home/wa/project/demo"}',
51
+ '{"type":"message","id":"u1","parentId":null,"message":{"role":"user","content":"第一条消息"}}',
52
+ '{"type":"session_info","id":"s1","parentId":"a1","name":"旧名字","timestamp":"2026-01-01T00:00:05.000Z"}',
53
+ '{"type":"session_info","id":"s2","parentId":"a1","name":"最终名字","timestamp":"2026-01-01T00:00:06.000Z"}',
54
+ ].join("\n") + "\n");
55
+
56
+ const result = await scanSessionFiles(root);
57
+ assert.equal(result.length, 1);
58
+ // 扫描直接读名字(反向分块),取最后一个 session_info。
59
+ assert.equal(result[0].name, "最终名字");
60
+ } finally {
61
+ rmSync(root, { recursive: true, force: true });
62
+ }
63
+ });
64
+
65
+ test("scanSessionFiles:改名后大量新消息把 session_info 推远也能读到(旧 bug 场景)", async () => {
66
+ const { root, projDir } = makeRoot();
67
+ try {
68
+ const file = join(projDir, "2026-01-01T00-00-00-000Z_far.jsonl");
69
+ const lines = [
70
+ '{"type":"session","version":3,"id":"far","timestamp":"2026-01-01T00:00:00.000Z","cwd":"/home/wa/project/demo"}',
71
+ '{"type":"session_info","id":"s1","parentId":"a1","name":"深藏的名字","timestamp":"2026-01-01T00:00:05.000Z"}',
72
+ ];
73
+ const filler = "x".repeat(1024);
74
+ for (let i = 0; i < 200; i++) {
75
+ lines.push(`{"type":"message","id":"m${i}","parentId":"p","message":{"role":"assistant","content":"${filler}"}}`);
76
+ }
77
+ writeFileSync(file, lines.join("\n") + "\n");
78
+
79
+ const result = await scanSessionFiles(root);
80
+ assert.equal(result.length, 1);
81
+ assert.equal(result[0].name, "深藏的名字");
82
+ } finally {
83
+ rmSync(root, { recursive: true, force: true });
84
+ }
85
+ });
86
+
87
+ test("scanSessionFiles:非会话文件(首行不是 session)跳过", async () => {
88
+ const { root, projDir } = makeRoot();
89
+ try {
90
+ writeFileSync(join(projDir, "junk.jsonl"), '{"type":"message",...}\n');
91
+ writeFileSync(join(projDir, "empty.jsonl"), "");
92
+ const result = await scanSessionFiles(root);
93
+ assert.equal(result.length, 0);
94
+ } finally {
95
+ rmSync(root, { recursive: true, force: true });
96
+ }
97
+ });
98
+
99
+ test("scanSessionFiles:超大正文不影响扫描,只取首部简述", async () => {
100
+ const { root, projDir } = makeRoot();
101
+ try {
102
+ const file = join(projDir, "2026-01-01T00-00-00-000Z_ccc.jsonl");
103
+ // 中间故意放一个巨大的 assistant 消息(模拟大会话文件)。
104
+ const hugeText = "x".repeat(1024 * 1024);
105
+ writeFileSync(file, [
106
+ '{"type":"session","version":3,"id":"ccc","timestamp":"2026-01-01T00:00:00.000Z","cwd":"/home/wa/project/demo"}',
107
+ '{"type":"message","id":"u1","parentId":null,"message":{"role":"user","content":"帮我写一篇文章"}}',
108
+ `{"type":"message","id":"a1","parentId":"u1","message":{"role":"assistant","content":[{"type":"text","text":"${hugeText}"}]}}`,
109
+ '{"type":"message","id":"u2","parentId":"a1","message":{"role":"user","content":"再改改"}}',
110
+ ].join("\n") + "\n");
111
+
112
+ const t0 = Date.now();
113
+ const result = await scanSessionFiles(root);
114
+ const elapsed = Date.now() - t0;
115
+ assert.equal(result.length, 1);
116
+ assert.equal(result[0].id, "ccc");
117
+ assert.equal(result[0].firstMessage, "帮我写一篇文章");
118
+ assert.ok(elapsed < 2000, `大文件扫描应远快于读全量(实际 ${elapsed}ms)`);
119
+ } finally {
120
+ rmSync(root, { recursive: true, force: true });
121
+ }
122
+ });
123
+
124
+ test("scanSessionFiles:按 modified(文件 mtime)降序排序", async () => {
125
+ const { root, projDir } = makeRoot();
126
+ try {
127
+ const old = join(projDir, "2026-01-01T00-00-00-000Z_old.jsonl");
128
+ const fresh = join(projDir, "2026-01-02T00-00-00-000Z_new.jsonl");
129
+ writeFileSync(old, '{"type":"session","version":3,"id":"old","timestamp":"2026-01-01T00:00:00.000Z","cwd":"/home/wa/project/demo"}\n');
130
+ // 新文件 mtime 更新。
131
+ await new Promise((resolve) => setTimeout(resolve, 30));
132
+ writeFileSync(fresh, '{"type":"session","version":3,"id":"new","timestamp":"2026-01-02T00:00:00.000Z","cwd":"/home/wa/project/demo"}\n');
133
+
134
+ const result = await scanSessionFiles(root);
135
+ assert.deepEqual(result.map((s) => s.id), ["new", "old"]);
136
+ } finally {
137
+ rmSync(root, { recursive: true, force: true });
138
+ }
139
+ });
140
+
141
+ test("scanSessionFiles:缺失 timestamp 的 header 用 mtime 兜底", async () => {
142
+ const { root, projDir } = makeRoot();
143
+ try {
144
+ writeFileSync(join(projDir, "2026-01-01T00-00-00-000Z_noTs.jsonl"), '{"type":"session","version":3,"id":"noTs","cwd":"/home/wa/project/demo"}\n');
145
+ const result = await scanSessionFiles(root);
146
+ assert.equal(result.length, 1);
147
+ assert.ok(Number.isFinite(result[0].created.getTime()), "created 不可为 Invalid Date");
148
+ } finally {
149
+ rmSync(root, { recursive: true, force: true });
150
+ }
151
+ });
@@ -0,0 +1,238 @@
1
+ import { closeSync, openSync, readSync, statSync } from "fs";
2
+ import { readdir } from "fs/promises";
3
+ import { join as joinPath } from "path";
4
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
5
+
6
+ /**
7
+ * 轻量会话列表扫描:只读取每个 jsonl 的「头部」(session header + 首条用户
8
+ * 消息简述)和「尾部」(自定义名 session_info,反向分块读),中间内容一律
9
+ * 不读,最后活动时间直接用文件 mtime。列表展示只需要标题 + 时间 + 名字。
10
+ */
11
+
12
+ interface HeaderEntry {
13
+ type: "session";
14
+ id?: unknown;
15
+ cwd?: unknown;
16
+ timestamp?: unknown;
17
+ parentSession?: unknown;
18
+ }
19
+
20
+ export interface SessionScanResult {
21
+ path: string;
22
+ id: string;
23
+ cwd: string;
24
+ name?: string;
25
+ created: Date;
26
+ modified: Date;
27
+ firstMessage: string;
28
+ /** 最后一条 assistant 回复文本(截断),无则为空串 */
29
+ lastReply: string;
30
+ parentSessionPath?: string;
31
+ }
32
+
33
+ // 初始读块:前几条 entry(header + 首条用户消息)通常远小于此;
34
+ // 不足时再按需扩读,直到拿到首条消息或达到上限。
35
+ const HEAD_INITIAL_BYTES = 4 * 1024;
36
+ const HEAD_MAX_BYTES = 48 * 1024;
37
+ // 列表里的首条消息只做简述展示(sidebar 也只截取前 50 字符)。
38
+ const FIRST_MESSAGE_PREVIEW_LENGTH = 300;
39
+
40
+ function parseLine(line: string): unknown {
41
+ if (!line.trim()) return null;
42
+ try {
43
+ return JSON.parse(line);
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ function extractTextContent(message: { content?: unknown }): string {
50
+ const content = message.content;
51
+ if (typeof content === "string") return content;
52
+ if (!Array.isArray(content)) return "";
53
+ return content
54
+ .filter((block) => block && block.type === "text" && typeof block.text === "string")
55
+ .map((block) => block.text)
56
+ .join(" ");
57
+ }
58
+
59
+ /**
60
+ * 读取文件头部:拿 header + 首条用户消息。
61
+ * 策略:先读小块(4KB),解析完整行;若首条消息已拿到则停;若块尾行
62
+ * 不完整(可能被截断),或还没拿到首条消息,则扩大读取继续。
63
+ * 首行必须是 session 头,否则视为非本应用会话文件。
64
+ */
65
+ function scanHead(path: string, size: number): { header: HeaderEntry; firstMessage: string } | null {
66
+ let readLen = Math.min(HEAD_INITIAL_BYTES, size);
67
+ let text = "";
68
+ const fd = openSync(path, "r");
69
+ try {
70
+ let header: HeaderEntry | null = null;
71
+ let firstMessage = "";
72
+
73
+ while (true) {
74
+ const buf = Buffer.alloc(readLen);
75
+ readSync(fd, buf, 0, readLen, 0);
76
+ const chunk = buf.toString("utf8");
77
+ // 块尾可能是半行:丢到最后完整换行处,避免解析半行。
78
+ const lastNl = chunk.lastIndexOf("\n");
79
+ const end = lastNl === -1 ? chunk.length : lastNl + 1;
80
+ text += chunk.slice(0, end);
81
+
82
+ for (const line of text.split("\n")) {
83
+ const entry = parseLine(line) as
84
+ | HeaderEntry
85
+ | { type?: unknown; message?: { role?: unknown; content?: unknown } }
86
+ | null;
87
+ if (!entry) continue;
88
+ if (!header) {
89
+ // 首条 entry 必须是 session 头,否则不是本应用的会话文件。
90
+ if (entry.type !== "session") return null;
91
+ header = entry as HeaderEntry;
92
+ continue;
93
+ }
94
+ if (entry.type === "message" && entry.message?.role === "user") {
95
+ const msgText = extractTextContent(entry.message).trim();
96
+ if (msgText) {
97
+ firstMessage = msgText.slice(0, FIRST_MESSAGE_PREVIEW_LENGTH);
98
+ break;
99
+ }
100
+ }
101
+ }
102
+
103
+ if (!header) return null;
104
+ if (firstMessage) break; // 已拿到首条消息,收工。
105
+
106
+ // 还没拿到首条消息:已读满上限或已到文件尾则放弃;否则扩读。
107
+ if (readLen >= HEAD_MAX_BYTES || readLen >= size) break;
108
+ readLen = Math.min(readLen * 2, HEAD_MAX_BYTES, size);
109
+ if (readLen <= 0) break;
110
+ }
111
+ return { header, firstMessage };
112
+ } finally {
113
+ closeSync(fd);
114
+ }
115
+ }
116
+
117
+ // 从文件末尾向前翻的块大小与上限。反向分块:session_info 是 append 写入的,
118
+ // 改名后继续聊会被推到更靠前的位置;从尾部向前找最后一个 session_info
119
+ // 与 SDK getSessionName 语义一致,且大多数会话第一块就命中(改名后没怎么聊)。
120
+ const NAME_BLOCK_BYTES = 64 * 1024;
121
+ const NAME_MAX_BLOCKS = 256; // 约 16MB,超出视为无名字
122
+
123
+ /** 读取文件尾部:最后一个 session_info 的自定义名 + 最后一条 assistant 回复。
124
+ * 同一趟反向分块里同时取两样:名字(显式清空 → undefined)、
125
+ * lastReply(最后一个 text 块,完整不截断)。返回 { name, lastReply }。 */
126
+ function scanTail(
127
+ path: string,
128
+ size: number,
129
+ ): { name: string | undefined; lastReply: string } {
130
+ const result: { name: string | undefined; lastReply: string } = { name: undefined, lastReply: "" };
131
+ if (size <= 0) return result;
132
+ const fd = openSync(path, "r");
133
+ try {
134
+ let offset = size;
135
+ for (let block = 0; block < NAME_MAX_BLOCKS && offset > 0; block++) {
136
+ const start = Math.max(0, offset - NAME_BLOCK_BYTES);
137
+ const len = offset - start;
138
+ const buf = Buffer.alloc(len);
139
+ readSync(fd, buf, 0, len, start);
140
+ const text = buf.toString("utf8");
141
+ const lines = text.split("\n");
142
+ // 块首可能截断一行(前一行的后半),跳过第一行;块尾对齐 offset 完整。
143
+ // 从后往前找本块内最后一个 session_info 与最后一条 assistant 回复。
144
+ for (let i = lines.length - 1; i >= 1; i--) {
145
+ if (result.name !== undefined && result.lastReply) continue;
146
+ const entry = parseLine(lines[i]) as
147
+ | { type?: string; name?: unknown; message?: { role?: unknown; content?: unknown } }
148
+ | null;
149
+ if (!entry) continue;
150
+ if (result.name === undefined && entry.type === "session_info" && typeof entry.name === "string") {
151
+ const name = entry.name.trim();
152
+ if (name) result.name = name; // 显式清空 → undefined
153
+ } else if (!result.lastReply && entry.type === "message" && entry.message?.role === "assistant") {
154
+ const reply = extractTextContent(entry.message).trim();
155
+ // 不截断:收合卡完整展示最后一条消息(跨 64KB 块的超大回复仍可能缺块首,极端场景)
156
+ if (reply) result.lastReply = reply;
157
+ }
158
+ if (result.name !== undefined && result.lastReply) break; // 两个都拿到,收工
159
+ }
160
+ offset = start;
161
+ }
162
+ return result;
163
+ } finally {
164
+ closeSync(fd);
165
+ }
166
+ }
167
+
168
+ function scanOneSessionFile(path: string): SessionScanResult | null {
169
+ let stat;
170
+ try {
171
+ stat = statSync(path);
172
+ } catch {
173
+ return null;
174
+ }
175
+ if (!stat.isFile() || stat.size === 0) return null;
176
+
177
+ const scanned = scanHead(path, stat.size);
178
+ if (!scanned) return null;
179
+ const { header, firstMessage } = scanned;
180
+
181
+ const id = typeof header.id === "string" ? header.id : "";
182
+ if (!id) return null;
183
+ const created = new Date(typeof header.timestamp === "string" ? header.timestamp : "");
184
+ if (Number.isNaN(created.getTime())) created.setTime(stat.mtime.getTime());
185
+
186
+ const tail = scanTail(path, stat.size);
187
+
188
+ return {
189
+ path,
190
+ id,
191
+ cwd: typeof header.cwd === "string" ? header.cwd : "",
192
+ name: tail.name,
193
+ created,
194
+ modified: stat.mtime,
195
+ firstMessage: firstMessage || "(no messages)",
196
+ lastReply: tail.lastReply,
197
+ parentSessionPath: typeof header.parentSession === "string" ? header.parentSession : undefined,
198
+ };
199
+ }
200
+
201
+ /**
202
+ * 扫描 <agentDir>/sessions 下所有 jsonl。同步小 IO 串行即可(单文件只读
203
+ * 头部小块 + 尾部反向分块),`sessionsDir` 仅在测试时注入。
204
+ */
205
+ export async function scanSessionFiles(sessionsDir?: string): Promise<SessionScanResult[]> {
206
+ const root = sessionsDir ?? joinPath(getAgentDir(), "sessions");
207
+ let projectDirs: string[];
208
+ try {
209
+ const entries = await readdir(root, { withFileTypes: true });
210
+ projectDirs = entries
211
+ .filter((entry) => entry.isDirectory() || entry.isSymbolicLink())
212
+ .map((entry) => joinPath(root, entry.name));
213
+ } catch {
214
+ return [];
215
+ }
216
+
217
+ const files: string[] = [];
218
+ for (const dir of projectDirs) {
219
+ try {
220
+ for (const name of await readdir(dir)) {
221
+ if (name.endsWith(".jsonl")) files.push(joinPath(dir, name));
222
+ }
223
+ } catch {
224
+ // 单目录不可读则跳过,不影响其它目录。
225
+ }
226
+ }
227
+
228
+ const results: SessionScanResult[] = [];
229
+ for (const file of files) {
230
+ const scanned = scanOneSessionFile(file);
231
+ if (scanned) results.push(scanned);
232
+ }
233
+ results.sort((a, b) => b.modified.getTime() - a.modified.getTime());
234
+ return results;
235
+ }
236
+
237
+ // 以对象形式暴露扫描入口,测试可替换实现(ESM 的模块绑定本身只读)。
238
+ export const sessionScanner = { scan: scanSessionFiles };
@@ -0,0 +1,173 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { DatabaseSync } from "node:sqlite";
7
+ import { createJiti } from "jiti";
8
+
9
+ const jiti = createJiti(import.meta.url, { interopDefault: true, moduleCache: false });
10
+ const { setDbForTesting } = await jiti.import("./sqlite-db.ts");
11
+ const { extractSessionText, ensureSearchIndex, searchSessions, fuzzyQueryForTerm } =
12
+ await jiti.import("./session-search.ts");
13
+
14
+ function freshDb() {
15
+ setDbForTesting(new DatabaseSync(":memory:"));
16
+ }
17
+
18
+ function makeSession(id, path, opts = {}) {
19
+ return {
20
+ path,
21
+ id,
22
+ cwd: "/tmp/proj",
23
+ name: opts.name,
24
+ created: "2026-08-27T00:00:00.000Z",
25
+ modified: opts.modified ?? "2026-08-27T01:00:00.000Z",
26
+ messageCount: 1,
27
+ firstMessage: opts.firstMessage ?? "",
28
+ };
29
+ }
30
+
31
+ const SAMPLE_LINES = [
32
+ '{"type":"session","version":3,"id":"s1","timestamp":"2026-08-27T00:00:00.000Z","cwd":"/tmp/proj"}',
33
+ '{"type":"message","id":"u1","parentId":null,"timestamp":"2026-08-27T00:00:01.000Z","message":{"role":"user","content":"修复登录接口的 bug"}}',
34
+ '{"type":"message","id":"a1","parentId":"u1","timestamp":"2026-08-27T00:00:02.000Z","message":{"role":"assistant","content":[{"type":"text","text":"我先改了 Token 逻辑"},{"type":"toolCall","id":"tc1","toolName":"read","input":{"path":"a.ts"}}]}}',
35
+ '{"type":"message","id":"r1","parentId":"a1","timestamp":"2026-08-27T00:00:03.000Z","message":{"role":"toolResult","toolCallId":"tc1","content":[{"type":"text","text":"文件内容 ok"},{"type":"image","source":{"type":"base64","data":"QUFBQUFBQUFB"}}]}}',
36
+ '{"type":"message","id":"b1","parentId":"r1","timestamp":"2026-08-27T00:00:04.000Z","message":{"role":"bashExecution","command":"npm test","output":"12 passed","excludeFromContext":true}}',
37
+ '{"type":"compaction","id":"c1","parentId":"b1","timestamp":"2026-08-27T00:00:05.000Z","summary":"跳过早期讨论","firstKeptEntryId":"u1","tokensBefore":100}',
38
+ ];
39
+
40
+ function fixtureDir(files) {
41
+ const dir = mkdtempSync(join(tmpdir(), "pi-search-test-"));
42
+ for (const [name, lines] of Object.entries(files)) {
43
+ writeFileSync(join(dir, name), lines.join("\n"));
44
+ }
45
+ return dir;
46
+ }
47
+
48
+ test("extractSessionText pulls title, messages, tools, bash, compaction; skips base64", () => {
49
+ const dir = fixtureDir({ "s1.jsonl": SAMPLE_LINES });
50
+ try {
51
+ const { title, body } = extractSessionText(makeSession("s1", join(dir, "s1.jsonl"), { name: "登录重构" }));
52
+ assert.equal(title, "登录重构");
53
+ assert.ok(body.includes("修复登录接口"), "user message");
54
+ assert.ok(body.includes("Token 逻辑"), "assistant text");
55
+ assert.ok(body.includes("[tool:read]"), "tool call name");
56
+ assert.ok(body.includes('"path":"a.ts"'), "tool call input");
57
+ assert.ok(body.includes("npm test"), "bash command");
58
+ assert.ok(body.includes("12 passed"), "bash output");
59
+ assert.ok(body.includes("跳过早期讨论"), "compaction summary");
60
+ assert.ok(!body.includes("QUFBQUFBQUFB"), "base64 image data must be dropped");
61
+ assert.ok(!body.includes("[image]"), "toolResult images must be omitted");
62
+ } finally {
63
+ rmSync(dir, { recursive: true, force: true });
64
+ }
65
+ });
66
+
67
+ test("ensureSearchIndex re-indexes on mtime change, drops vanished sessions", async () => {
68
+ freshDb();
69
+ const dir = fixtureDir({ "s1.jsonl": SAMPLE_LINES });
70
+ try {
71
+ const session = makeSession("s1", join(dir, "s1.jsonl"), { name: "登录重构" });
72
+ const first = await ensureSearchIndex([session]);
73
+ assert.equal(first.indexing, true);
74
+ const again = await ensureSearchIndex([session]);
75
+ assert.equal(again.indexing, false, "unchanged mtime must be a no-op");
76
+
77
+ const modified = { ...session, modified: "2026-08-27T02:00:00.000Z" };
78
+ const reindexed = await ensureSearchIndex([modified]);
79
+ assert.equal(reindexed.indexing, true, "mtime change must re-index");
80
+
81
+ const dropped = await ensureSearchIndex([]);
82
+ assert.equal(dropped.indexing, true);
83
+ const { results } = await searchSessions("登录接口", 10, []);
84
+ assert.equal(results.length, 0, "index must drop vanished sessions");
85
+ } finally {
86
+ rmSync(dir, { recursive: true, force: true });
87
+ }
88
+ });
89
+
90
+ test("searchSessions matches Chinese substring, 2-char LIKE fallback, English, titleMatch", async () => {
91
+ freshDb();
92
+ const dir = fixtureDir({ "s1.jsonl": SAMPLE_LINES });
93
+ try {
94
+ const s1 = makeSession("s1", join(dir, "s1.jsonl"), { name: "登录重构" });
95
+ const s2 = {
96
+ ...makeSession("s2", join(dir, "missing.jsonl"), {}),
97
+ name: "SQLite FTS index",
98
+ };
99
+ const sessions = [s1, s2];
100
+
101
+ // 4-char Chinese substring in body — title "登录重构" does NOT contain it,
102
+ // so this is a pure body hit.
103
+ const { results: m1 } = await searchSessions("登录接口", 10, sessions);
104
+ assert.deepEqual(m1.map((r) => r.session.id), ["s1"]);
105
+ assert.equal(m1[0].titleMatch, false);
106
+ assert.ok(m1[0].snippet.length > 0, "snippet should carry highlight markers");
107
+
108
+ // Title-hitting 4-char query sets titleMatch.
109
+ const { results: mTitle } = await searchSessions("登录重构", 10, sessions);
110
+ assert.deepEqual(mTitle.map((r) => r.session.id), ["s1"]);
111
+ assert.equal(mTitle[0].titleMatch, true);
112
+
113
+ // 2-char Chinese query — MATCH misses, LIKE fallback hits.
114
+ const { results: m2 } = await searchSessions("登录", 10, sessions);
115
+ assert.deepEqual(m2.map((r) => r.session.id), ["s1"]);
116
+
117
+ // English word.
118
+ const { results: m3 } = await searchSessions("fts", 10, sessions);
119
+ assert.deepEqual(m3.map((r) => r.session.id), ["s2"]);
120
+ assert.equal(m3[0].titleMatch, true);
121
+
122
+ // No results.
123
+ const { results: none } = await searchSessions("不存在的词xyz", 10, sessions);
124
+ assert.equal(none.length, 0);
125
+
126
+ // Quoted/special chars must not throw.
127
+ const { results: safe } = await searchSessions('a"b', 10, sessions);
128
+ assert.equal(safe.length, 0);
129
+ } finally {
130
+ rmSync(dir, { recursive: true, force: true });
131
+ }
132
+ });
133
+
134
+ test("searchSessions 多关键字空格分隔 AND(FTS 与 LIKE 回退)", async () => {
135
+ freshDb();
136
+ const dir = fixtureDir({ "s1.jsonl": SAMPLE_LINES });
137
+ try {
138
+ const s1 = makeSession("s1", join(dir, "s1.jsonl"), { name: "登录重构" });
139
+ const s2 = {
140
+ ...makeSession("s2", join(dir, "missing.jsonl"), {}),
141
+ name: "SQLite FTS index",
142
+ };
143
+ const sessions = [s1, s2];
144
+
145
+ // FTS 路径(两词均 ≥3 字符,且都在 s1 body 命中):"登录接口 token" → s1
146
+ const { results: fts } = await searchSessions("登录接口 token", 10, sessions);
147
+ assert.deepEqual(fts.map((r) => r.session.id), ["s1"], "FTS 多词 AND");
148
+
149
+ // LIKE 回退(含 2 字符词)title 命中:"登录 重构" → s1(title="登录重构")
150
+ const { results: likeTitle } = await searchSessions("登录 重构", 10, sessions);
151
+ assert.deepEqual(likeTitle.map((r) => r.session.id), ["s1"], "LIKE 回退多词 AND title");
152
+ assert.equal(likeTitle[0].titleMatch, true);
153
+
154
+ // LIKE 回退 body 场景:"登录 token" → s1(body 含两词)
155
+ const { results: likeBody } = await searchSessions("登录 token", 10, sessions);
156
+ assert.deepEqual(likeBody.map((r) => r.session.id), ["s1"], "LIKE 回退多词 AND body");
157
+
158
+ // 缺任一词不命中:s1 无 "不存在词xyz"
159
+ const { results: miss } = await searchSessions("登录接口 不存在词xyz", 10, sessions);
160
+ assert.equal(miss.length, 0, "缺一词不命中");
161
+
162
+ // 多空格/首尾空格容忍
163
+ const { results: spaced } = await searchSessions(" 登录 token ", 10, sessions);
164
+ assert.deepEqual(spaced.map((r) => r.session.id), ["s1"], "多空格容忍");
165
+ } finally {
166
+ rmSync(dir, { recursive: true, force: true });
167
+ }
168
+ });
169
+
170
+ test("fuzzyQueryForTerm escapes quotes into an FTS phrase", () => {
171
+ assert.equal(fuzzyQueryForTerm('a"b'), '"a""b"');
172
+ assert.equal(fuzzyQueryForTerm("登录"), '"登录"');
173
+ });