@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,548 @@
1
+ import {
2
+ SessionManager,
3
+ buildContextEntries as piBuildContextEntries,
4
+ buildSessionContext as piBuildSessionContext,
5
+ getAgentDir,
6
+ } from "@earendil-works/pi-coding-agent";
7
+ import { closeSync, existsSync, openSync, readSync } from "fs";
8
+ import { readdir } from "fs/promises";
9
+ import { join as joinPath, normalize as normalizePath } from "path";
10
+ import type { AgentMessage, SessionEntry, SessionHeader, SessionInfo, SessionContext, TodoItem } from "./types";
11
+ import type { SessionEntry as PiSessionEntry } from "@earendil-works/pi-coding-agent";
12
+ import { getDb } from "./sqlite-db";
13
+ import { normalizeToolCalls } from "./normalize";
14
+ import { projectIdentityKey } from "./project-identity";
15
+ import { sessionPathKey } from "./session-path";
16
+ import { resolveProject, type ProjectInfo } from "./worktree";
17
+ import { scanSessionFiles, sessionScanner } from "./session-scanner";
18
+
19
+ export { getAgentDir };
20
+
21
+ export async function attachSessionProjectInfo(sessions: SessionInfo[]): Promise<SessionInfo[]> {
22
+ const uniqueCwds = [...new Set(sessions.map((s) => s.cwd).filter(Boolean))];
23
+ const projectByCwd = new Map<string, ProjectInfo>();
24
+ await Promise.all(uniqueCwds.map(async (cwd) => {
25
+ projectByCwd.set(cwd, await resolveProject(cwd));
26
+ }));
27
+
28
+ // Pinned set from the task store (region-relative ordering lives client-side).
29
+ let pinnedIds = new Set<string>();
30
+ try {
31
+ const rows = getDb()
32
+ .prepare("SELECT session_id FROM session_meta WHERE pinned = 1")
33
+ .all() as { session_id: string }[];
34
+ pinnedIds = new Set(rows.map((r) => r.session_id));
35
+ } catch {
36
+ // db not available — fall back to nothing pinned
37
+ }
38
+
39
+ return sessions.map((session) => {
40
+ const project = session.cwd ? projectByCwd.get(session.cwd) : undefined;
41
+ const projectRoot = project?.projectRoot ?? session.cwd;
42
+ return {
43
+ ...session,
44
+ projectRoot,
45
+ projectKey: projectIdentityKey(projectRoot),
46
+ ...(project?.branch ? { branch: project.branch } : {}),
47
+ ...(project?.isWorktree ? { isWorktree: true } : {}),
48
+ ...(project?.isWorktree && project.branch ? { worktreeBranch: project.branch } : {}),
49
+ ...(pinnedIds.has(session.id) ? { pinned: true } : {}),
50
+ };
51
+ });
52
+ }
53
+
54
+ export function mergeSessionLists(
55
+ persistedSessions: SessionInfo[],
56
+ supplementalSessions: SessionInfo[],
57
+ ): SessionInfo[] {
58
+ const byId = new Map(supplementalSessions.map((session) => [session.id, session]));
59
+ // A disk scan is authoritative once the JSONL exists. In particular, this
60
+ // replaces a transient registry snapshot without briefly rendering two rows.
61
+ for (const session of persistedSessions) byId.set(session.id, session);
62
+ return [...byId.values()].sort((a, b) => b.modified.localeCompare(a.modified));
63
+ }
64
+
65
+ async function loadAllSessions(): Promise<SessionInfo[]> {
66
+ // 轻量扫描:只读每个会话文件的头部(header + 首条用户消息)与尾部(最新
67
+ // session_info 自定义名),最后活动时间用文件 mtime——列表只展示标题+时间,
68
+ // 不读取消息正文(SDK 的 listAll 会全量读每个 jsonl 并拼接全部文本,在大会话
69
+ // 文件上会把列表刷新拖到秒级甚至十几秒)。
70
+ const scanned = await sessionScanner.scan();
71
+ const pathToId = new Map<string, string>();
72
+ for (const s of scanned) pathToId.set(sessionPathKey(s.path), s.id);
73
+
74
+ const sessions = scanned.map((s) => {
75
+ cacheSessionPath(s.id, s.path);
76
+ return {
77
+ path: s.path,
78
+ id: s.id,
79
+ cwd: s.cwd,
80
+ name: s.name,
81
+ created: s.created.toISOString(),
82
+ modified: s.modified.toISOString(),
83
+ // 列表不消费消息数(精确值需要全量读,已移除);保留字段以兼容类型。
84
+ messageCount: 0,
85
+ firstMessage: s.firstMessage || "(no messages)",
86
+ lastReply: s.lastReply || "",
87
+ parentSessionId: s.parentSessionPath ? pathToId.get(sessionPathKey(s.parentSessionPath)) : undefined,
88
+ transient: false,
89
+ };
90
+ });
91
+ return attachSessionProjectInfo(sessions);
92
+ }
93
+
94
+ export async function listAllSessions(options: { force?: boolean } = {}): Promise<SessionInfo[]> {
95
+ if (options.force) invalidateSessionListCache();
96
+ const generation = globalThis.__piSessionListGeneration ?? 0;
97
+
98
+ // Return cached result if still fresh (avoids re-scanning session files
99
+ // and re-spawning git processes on every page load).
100
+ if (globalThis.__piSessionListCache && Date.now() - globalThis.__piSessionListCache.ts < SESSION_LIST_CACHE_TTL_MS) {
101
+ return globalThis.__piSessionListCache.data;
102
+ }
103
+
104
+ // Coalescing dedup: concurrent callers share the same in-flight promise
105
+ // only while it belongs to the current cache generation.
106
+ if (globalThis.__piSessionListPromise && globalThis.__piSessionListPromiseGeneration === generation) {
107
+ return globalThis.__piSessionListPromise;
108
+ }
109
+
110
+ const loadPromise = loadAllSessions().then((data) => {
111
+ // If a mutation invalidated this scan, make this caller join (or start) a
112
+ // scan for the current generation. Returning the stale result here made a
113
+ // refresh race indistinguishable from a successful refresh.
114
+ if ((globalThis.__piSessionListGeneration ?? 0) !== generation) {
115
+ return listAllSessions();
116
+ }
117
+ globalThis.__piSessionListCache = { data, ts: Date.now() };
118
+ return data;
119
+ });
120
+ const trackedPromise = loadPromise.finally(() => {
121
+ if (globalThis.__piSessionListPromise === trackedPromise) {
122
+ globalThis.__piSessionListPromise = undefined;
123
+ globalThis.__piSessionListPromiseGeneration = undefined;
124
+ }
125
+ });
126
+
127
+ globalThis.__piSessionListPromise = trackedPromise;
128
+ globalThis.__piSessionListPromiseGeneration = generation;
129
+ return trackedPromise;
130
+ }
131
+
132
+ // ============================================================================
133
+ // Session path caches, stored in globalThis for hot-reload safety.
134
+ // ============================================================================
135
+ declare global {
136
+ var __piSessionPathCache: Map<string, string> | undefined;
137
+ var __piPathToSessionIdCache: Map<string, string> | undefined;
138
+ var __piSessionListPromise: Promise<SessionInfo[]> | undefined;
139
+ var __piSessionListPromiseGeneration: number | undefined;
140
+ var __piSessionListGeneration: number | undefined;
141
+ var __piSessionListCache: { data: SessionInfo[]; ts: number } | undefined;
142
+ }
143
+
144
+ const SESSION_LIST_CACHE_TTL_MS = 300_000;
145
+
146
+ export function invalidateSessionListCache(): void {
147
+ globalThis.__piSessionListGeneration = (globalThis.__piSessionListGeneration ?? 0) + 1;
148
+ globalThis.__piSessionListCache = undefined;
149
+ }
150
+
151
+ function getPathCache(): Map<string, string> {
152
+ if (!globalThis.__piSessionPathCache) globalThis.__piSessionPathCache = new Map();
153
+ return globalThis.__piSessionPathCache;
154
+ }
155
+
156
+ function getPathToIdCache(): Map<string, string> {
157
+ if (!globalThis.__piPathToSessionIdCache) globalThis.__piPathToSessionIdCache = new Map();
158
+ return globalThis.__piPathToSessionIdCache;
159
+ }
160
+
161
+ /**
162
+ * Find one session's file without parsing the catalogue.
163
+ *
164
+ * Session files are written as `<timestamp>_<id>.jsonl` under a per-project
165
+ * directory, so the id can be located by reading directory entries alone. The
166
+ * header is then parsed — bounded, first line only — to confirm the match
167
+ * rather than trusting the name. Returns null when nothing matches, leaving the
168
+ * caller on the full scan.
169
+ *
170
+ * `sessionId` is only ever compared against names that came back from
171
+ * `readdir`, never joined into a path itself, so a separator or `..` inside it
172
+ * cannot reach the filesystem.
173
+ */
174
+ async function findSessionPathByName(sessionId: string): Promise<string | null> {
175
+ // The SDK keeps `getSessionsDir` internal, but it is `<agentDir>/sessions`,
176
+ // the same way the other agent-dir paths are derived in this codebase.
177
+ const sessionsDir = joinPath(getAgentDir(), "sessions");
178
+ if (!sessionId || !existsSync(sessionsDir)) return null;
179
+
180
+ const suffix = `_${sessionId}.jsonl`;
181
+ let projectDirs;
182
+ try {
183
+ projectDirs = await readdir(sessionsDir, { withFileTypes: true });
184
+ } catch {
185
+ return null;
186
+ }
187
+
188
+ for (const entry of projectDirs) {
189
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
190
+ const dir = joinPath(sessionsDir, entry.name);
191
+ let names: string[];
192
+ try {
193
+ names = await readdir(dir);
194
+ } catch {
195
+ continue;
196
+ }
197
+ const match = names.find((name) => name.endsWith(suffix));
198
+ if (!match) continue;
199
+
200
+ const candidate = joinPath(dir, match);
201
+ try {
202
+ if (readSessionHeader(candidate)?.id === sessionId) return candidate;
203
+ } catch {
204
+ // Unreadable or truncated: let the full scan decide.
205
+ }
206
+ }
207
+ return null;
208
+ }
209
+
210
+ export async function resolveSessionPath(sessionId: string): Promise<string | null> {
211
+ const cached = getPathCache().get(sessionId);
212
+ if (cached) return cached;
213
+
214
+ // Opening one session should not wait for the whole catalogue to be parsed.
215
+ // The name carries the id, so this costs a directory listing per project plus
216
+ // one header read, and only a miss falls through to the full scan.
217
+ const direct = await findSessionPathByName(sessionId);
218
+ if (direct) {
219
+ cacheSessionPath(sessionId, direct);
220
+ return direct;
221
+ }
222
+
223
+ // Cache miss: scan all sessions to populate cache, then retry
224
+ await listAllSessions();
225
+ return getPathCache().get(sessionId) ?? null;
226
+ }
227
+
228
+ export async function resolveSessionIdByPath(filePath: string): Promise<string | undefined> {
229
+ const pathKey = sessionPathKey(filePath);
230
+ const cached = getPathToIdCache().get(pathKey);
231
+ if (cached) return cached;
232
+
233
+ await listAllSessions();
234
+ return getPathToIdCache().get(pathKey);
235
+ }
236
+
237
+ export function cacheSessionPath(sessionId: string, filePath: string): void {
238
+ const normalizedPath = normalizePath(filePath);
239
+ const pathKey = sessionPathKey(normalizedPath);
240
+ const pathCache = getPathCache();
241
+ const reverseCache = getPathToIdCache();
242
+ const previousPath = pathCache.get(sessionId);
243
+ const previousPathKey = previousPath ? sessionPathKey(previousPath) : undefined;
244
+ const previousSessionId = reverseCache.get(pathKey);
245
+ const previousOwnerPath = previousSessionId ? pathCache.get(previousSessionId) : undefined;
246
+ if (previousPathKey && previousPathKey !== pathKey && reverseCache.get(previousPathKey) === sessionId) {
247
+ reverseCache.delete(previousPathKey);
248
+ }
249
+ if (
250
+ previousSessionId &&
251
+ previousSessionId !== sessionId &&
252
+ previousOwnerPath &&
253
+ sessionPathKey(previousOwnerPath) === pathKey
254
+ ) {
255
+ pathCache.delete(previousSessionId);
256
+ }
257
+ pathCache.set(sessionId, normalizedPath);
258
+ reverseCache.set(pathKey, sessionId);
259
+ }
260
+
261
+ export function invalidateSessionPathCache(sessionId: string): void {
262
+ const pathCache = getPathCache();
263
+ const reverseCache = getPathToIdCache();
264
+ const filePath = pathCache.get(sessionId);
265
+ pathCache.delete(sessionId);
266
+ const pathKey = filePath ? sessionPathKey(filePath) : undefined;
267
+ if (pathKey && reverseCache.get(pathKey) === sessionId) {
268
+ reverseCache.delete(pathKey);
269
+ }
270
+ }
271
+
272
+ export function readSessionHeader(filePath: string): SessionHeader | null {
273
+ const fd = openSync(filePath, "r");
274
+ try {
275
+ const chunks: Buffer[] = [];
276
+ const maxHeaderBytes = 64 * 1024;
277
+ let position = 0;
278
+ let foundNewline = false;
279
+
280
+ while (position < maxHeaderBytes && !foundNewline) {
281
+ const buffer = Buffer.allocUnsafe(Math.min(4096, maxHeaderBytes - position));
282
+ const bytesRead = readSync(fd, buffer, 0, buffer.length, position);
283
+ if (bytesRead === 0) break;
284
+ const data = buffer.subarray(0, bytesRead);
285
+ const newlineIndex = data.indexOf(0x0a);
286
+ chunks.push(newlineIndex === -1 ? data : data.subarray(0, newlineIndex));
287
+ position += bytesRead;
288
+ foundNewline = newlineIndex !== -1;
289
+ }
290
+
291
+ if (!foundNewline && position >= maxHeaderBytes) return null;
292
+ const firstLine = Buffer.concat(chunks).toString("utf8").trimEnd();
293
+ if (!firstLine) return null;
294
+ try {
295
+ const header = JSON.parse(firstLine) as SessionHeader;
296
+ return header.type === "session" ? header : null;
297
+ } catch {
298
+ return null;
299
+ }
300
+ } finally {
301
+ closeSync(fd);
302
+ }
303
+ }
304
+
305
+ export function getSessionEntries(filePath: string): SessionEntry[] {
306
+ const entries = SessionManager.open(filePath).getEntries();
307
+ return entries as unknown as SessionEntry[];
308
+ }
309
+
310
+ export function extractTodosFromEntries(entries: SessionEntry[]): TodoItem[] {
311
+ // pi todo state is stored as {"type":"custom","customType":"pi-todo.state","data":{"todos":[...]}}
312
+ // entries appended to the session file — pick the latest snapshot. Todo
313
+ // entries are otherwise dropped from chat history (entryToUiMessage returns null).
314
+ let todos: TodoItem[] = [];
315
+ for (const entry of entries) {
316
+ if (entry.type !== "custom" || entry.customType !== "pi-todo.state") continue;
317
+ const data = (entry as { data?: unknown }).data;
318
+ if (!isRecord(data) || !Array.isArray(data.todos)) continue;
319
+ todos = data.todos.filter((t: unknown): t is TodoItem => (
320
+ isRecord(t) && typeof t.content === "string"
321
+ ));
322
+ }
323
+ return todos;
324
+ }
325
+
326
+ export function buildSessionContext(
327
+ entries: SessionEntry[],
328
+ leafId?: string | null,
329
+ options: { deferThinking?: boolean; deferToolResultImages?: boolean; tail?: number; excludeLeaf?: boolean } = {},
330
+ ): SessionContext {
331
+ const { tail, excludeLeaf } = options;
332
+ // Restrict the input to the active leaf's ancestor chain, capped at `tail`.
333
+ // SDK buildSessionContext only consumes this chain, so feeding it the full
334
+ // forest forces O(n) work and, for a linear session, O(n) recursion depth in
335
+ // any caller that rebuilds the path. Slicing here bounds both to O(tail).
336
+ const sliced = tail && tail > 0 ? sliceActiveBranch(entries, leafId ?? null, tail, excludeLeaf) : entries;
337
+ const byId = new Map<string, SessionEntry>();
338
+ for (const e of sliced) byId.set(e.id, e);
339
+
340
+ const piEntries = sliced as unknown as PiSessionEntry[];
341
+ const piCtx = piBuildSessionContext(piEntries, leafId, byId as unknown as Map<string, PiSessionEntry>);
342
+
343
+ const contextEntries = piBuildContextEntries(
344
+ piEntries,
345
+ leafId,
346
+ byId as unknown as Map<string, PiSessionEntry>,
347
+ );
348
+
349
+ // Convert the SDK-selected context entries and their IDs together. This keeps
350
+ // fork/navigation targets aligned while preserving pi's compaction ordering.
351
+ const messages: AgentMessage[] = [];
352
+ const entryIds: string[] = [];
353
+ const parentIds: (string | null)[] = [];
354
+ for (const entry of contextEntries) {
355
+ const localEntry = entry as unknown as SessionEntry;
356
+ const m = entryToUiMessage(localEntry, options);
357
+ if (m) {
358
+ messages.push(m);
359
+ entryIds.push(localEntry.id);
360
+ parentIds.push(localEntry.parentId);
361
+ }
362
+ }
363
+
364
+ // Collect the latest pi-todo.state snapshot from the active branch.
365
+ const todos = extractTodosFromEntries(contextEntries as unknown as SessionEntry[]);
366
+
367
+ return {
368
+ messages,
369
+ entryIds,
370
+ parentIds,
371
+ thinkingLevel: piCtx.thinkingLevel,
372
+ model: piCtx.model,
373
+ todos,
374
+ };
375
+ }
376
+
377
+ /**
378
+ * Extract the ancestor chain from `leafId` back toward the root, capped at
379
+ * `tail` entries (most-recent first after the final reverse). Iterative: a
380
+ * linear session's chain length equals its entry count, so a recursive walk
381
+ * would overflow the stack. The result is still a valid prefix of the active
382
+ * branch — older history is loaded on demand via pagination.
383
+ */
384
+ export function sliceActiveBranch(
385
+ entries: SessionEntry[],
386
+ leafId: string | null,
387
+ tail: number,
388
+ excludeLeaf = false,
389
+ ): SessionEntry[] {
390
+ if (tail <= 0) return entries;
391
+ const byId = new Map<string, SessionEntry>();
392
+ for (const e of entries) byId.set(e.id, e);
393
+
394
+ let leaf = leafId ? byId.get(leafId) : entries[entries.length - 1];
395
+ // Pagination: `before` is the oldest entry already loaded, so the next page
396
+ // must start at its parent to avoid duplicating `before` when prepended.
397
+ if (excludeLeaf && leaf?.parentId) leaf = byId.get(leaf.parentId);
398
+ if (!leaf) return [];
399
+ const chain: SessionEntry[] = [];
400
+ let current: SessionEntry | undefined = leaf;
401
+ while (current && chain.length < tail) {
402
+ chain.push(current);
403
+ current = current.parentId ? byId.get(current.parentId) : undefined;
404
+ }
405
+ chain.reverse();
406
+ return chain;
407
+ }
408
+
409
+ /**
410
+ * Whether paging `tail` ancestors from `leafId` (or its parent when
411
+ * `excludeLeaf` is set) would hit a cap — i.e. there is older history beyond
412
+ * the current window. The `tail` is measured on *entries*, and UI messages can
413
+ * filter entries down further, so the client cannot infer this from its own
414
+ * message count; the server is the source of truth.
415
+ */
416
+ export function hasOlderHistory(
417
+ entries: SessionEntry[],
418
+ leafId: string | null,
419
+ tail: number,
420
+ excludeLeaf = false,
421
+ ): boolean {
422
+ if (tail <= 0) return false;
423
+ const byId = new Map<string, SessionEntry>();
424
+ for (const e of entries) byId.set(e.id, e);
425
+ let cur: SessionEntry | undefined = leafId ? byId.get(leafId) : entries[entries.length - 1];
426
+ if (excludeLeaf && cur?.parentId) cur = byId.get(cur.parentId);
427
+ let count = 0;
428
+ while (cur) {
429
+ count++;
430
+ if (count > tail) return true;
431
+ cur = cur.parentId ? byId.get(cur.parentId) : undefined;
432
+ }
433
+ return false;
434
+ }
435
+
436
+ function parseEntryTimestamp(timestamp: string): number | undefined {
437
+ const parsed = Date.parse(timestamp);
438
+ return Number.isNaN(parsed) ? undefined : parsed;
439
+ }
440
+
441
+ function isRecord(value: unknown): value is Record<string, unknown> {
442
+ return typeof value === "object" && value !== null && !Array.isArray(value);
443
+ }
444
+
445
+ function base64ImageInfo(block: unknown): { bytes: number; mime?: string } | null {
446
+ if (!isRecord(block) || block.type !== "image") return null;
447
+
448
+ let data: string | undefined;
449
+ let mime: string | undefined;
450
+ if (typeof block.data === "string") {
451
+ data = block.data;
452
+ mime = typeof block.mimeType === "string" ? block.mimeType : undefined;
453
+ } else if (isRecord(block.source) && block.source.type === "base64" && typeof block.source.data === "string") {
454
+ data = block.source.data;
455
+ mime = typeof block.source.media_type === "string" ? block.source.media_type : undefined;
456
+ }
457
+ if (!data) return null;
458
+
459
+ const padding = data.endsWith("==") ? 2 : data.endsWith("=") ? 1 : 0;
460
+ return { bytes: Math.max(0, Math.floor(data.length * 3 / 4) - padding), mime };
461
+ }
462
+
463
+ function omitToolResultBase64Images(message: AgentMessage): AgentMessage {
464
+ if (message.role !== "toolResult") return message;
465
+
466
+ let omitted = 0;
467
+ let bytes = 0;
468
+ const mimes = new Set<string>();
469
+ const content = message.content.filter((block) => {
470
+ const image = base64ImageInfo(block);
471
+ if (!image) return true;
472
+ omitted += 1;
473
+ bytes += image.bytes;
474
+ if (image.mime) mimes.add(image.mime);
475
+ return false;
476
+ });
477
+ if (omitted === 0) return message;
478
+
479
+ const mimeText = mimes.size > 0 ? `: ${[...mimes].join(", ")}` : "";
480
+ content.push({
481
+ type: "text",
482
+ text: `[${omitted} tool result image${omitted === 1 ? "" : "s"} omitted from initial history payload${mimeText}, ~${bytes} bytes]`,
483
+ });
484
+ return { ...message, content };
485
+ }
486
+
487
+ // Convert a session entry on the active branch into a UI message.
488
+ // Returns null for entries that do not map to chat history (metadata, non-message types).
489
+ function entryToUiMessage(
490
+ entry: SessionEntry,
491
+ options: { deferThinking?: boolean; deferToolResultImages?: boolean },
492
+ ): AgentMessage | null {
493
+ // Supported message roles: user, assistant, toolResult, bashExecution.
494
+ // bashExecution messages enter the case "message" branch (entry.type === "message").
495
+ // The early return at line below ("!options.deferThinking || message.role !== "assistant"")
496
+ // passes non-assistant messages — including bashExecution — through unchanged.
497
+ // normalizeToolCalls is a secondary guard (returns non-assistant messages as-is).
498
+ switch (entry.type) {
499
+ case "message": {
500
+ const message = options.deferToolResultImages
501
+ ? omitToolResultBase64Images(normalizeToolCalls(entry.message))
502
+ : normalizeToolCalls(entry.message);
503
+ if (!options.deferThinking || message.role !== "assistant") return message;
504
+ // Real sessions may store assistant content as a string (not a block array),
505
+ // so guard the block-level transform instead of assuming an array.
506
+ const content = message.content;
507
+ if (!Array.isArray(content)) return message;
508
+ return {
509
+ ...message,
510
+ content: content.map((block) => (
511
+ block.type === "thinking" && block.thinking.trim() !== ""
512
+ ? { ...block, thinking: "", deferred: true }
513
+ : block
514
+ )),
515
+ };
516
+ }
517
+ case "compaction":
518
+ return {
519
+ role: "custom",
520
+ customType: "compaction",
521
+ content: entry.summary,
522
+ display: true,
523
+ details: {
524
+ tokensBefore: entry.tokensBefore,
525
+ firstKeptEntryId: entry.firstKeptEntryId,
526
+ },
527
+ timestamp: parseEntryTimestamp(entry.timestamp),
528
+ };
529
+ case "branch_summary":
530
+ if (!entry.summary) return null;
531
+ return {
532
+ role: "user",
533
+ content: `*The conversation briefly explored another branch and returned with this summary:*\n\n${entry.summary}`,
534
+ timestamp: parseEntryTimestamp(entry.timestamp),
535
+ };
536
+ case "custom_message":
537
+ return {
538
+ role: "custom",
539
+ customType: entry.customType,
540
+ content: entry.content,
541
+ display: entry.display,
542
+ details: entry.details,
543
+ timestamp: parseEntryTimestamp(entry.timestamp),
544
+ };
545
+ default:
546
+ return null;
547
+ }
548
+ }
@@ -0,0 +1,65 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test, { after } from "node:test";
6
+ import { createJiti } from "jiti";
7
+
8
+ // getAgentDir() reads this on every call, so pointing it at a scratch tree
9
+ // keeps the suite off the developer's real session catalogue.
10
+ const agentDir = mkdtempSync(join(tmpdir(), "pi-web-sessions-"));
11
+ const sessionsDir = join(agentDir, "sessions");
12
+ mkdirSync(sessionsDir, { recursive: true });
13
+ process.env.PI_CODING_AGENT_DIR = agentDir;
14
+ after(() => rmSync(agentDir, { recursive: true, force: true }));
15
+
16
+ function writeSession(project, id, { entries = 0, header } = {}) {
17
+ const dir = join(sessionsDir, project);
18
+ mkdirSync(dir, { recursive: true });
19
+ const file = join(dir, `2026-08-17T00-00-00_${id}.jsonl`);
20
+ const lines = [
21
+ JSON.stringify(
22
+ header ?? { type: "session", id, timestamp: new Date().toISOString(), cwd: join("/w", project) },
23
+ ),
24
+ ];
25
+ // Body the targeted lookup must never need to read.
26
+ for (let i = 0; i < entries; i++) {
27
+ lines.push(JSON.stringify({ type: "message", id: `m${i}`, parentId: null, payload: "x".repeat(256) }));
28
+ }
29
+ writeFileSync(file, lines.join("\n") + "\n");
30
+ return file;
31
+ }
32
+
33
+ const wanted = writeSession("alpha", "session-wanted", { entries: 3 });
34
+ writeSession("beta", "session-other", { entries: 200 });
35
+ const inGamma = writeSession("gamma", "session-deep");
36
+ // Named for one id, headed with another.
37
+ writeSession("alpha", "claimed-id", {
38
+ header: { type: "session", id: "actual-id", timestamp: new Date().toISOString(), cwd: "/w/alpha" },
39
+ });
40
+
41
+ const jiti = createJiti(import.meta.url);
42
+ const { resolveSessionPath } = await jiti.import("./session-reader.ts");
43
+
44
+ test("resolves a session path without parsing the catalogue", async () => {
45
+ assert.equal(await resolveSessionPath("session-wanted"), wanted);
46
+ });
47
+
48
+ test("finds a session in any project directory", async () => {
49
+ assert.equal(await resolveSessionPath("session-deep"), inGamma);
50
+ });
51
+
52
+ test("does not trust a filename whose header disagrees", async () => {
53
+ // The name matches, the header does not — the id must not resolve to it.
54
+ assert.notEqual(await resolveSessionPath("claimed-id"), join(sessionsDir, "alpha", "2026-08-17T00-00-00_claimed-id.jsonl"));
55
+ });
56
+
57
+ test("returns null for an unknown session rather than guessing", async () => {
58
+ assert.equal(await resolveSessionPath("no-such-session"), null);
59
+ });
60
+
61
+ test("a session id containing path separators cannot escape the sessions dir", async () => {
62
+ for (const hostile of ["../../etc/passwd", "..\\..\\windows", "a/../../b", ""]) {
63
+ assert.equal(await resolveSessionPath(hostile), null, `${hostile} must not resolve`);
64
+ }
65
+ });
@@ -0,0 +1,46 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ const {
5
+ SESSION_ROW_CONTEXT_MENU_EVENT,
6
+ dispatchSessionRowContextMenu,
7
+ } = await import("./session-row-context-menu.ts");
8
+
9
+ function createDetail() {
10
+ return {
11
+ id: "session-1",
12
+ path: "/tmp/session-1.jsonl",
13
+ cwd: "/tmp/project",
14
+ name: "Example session",
15
+ clientX: 120,
16
+ clientY: 80,
17
+ refresh: () => {},
18
+ };
19
+ }
20
+
21
+ test("leaves the native context menu unclaimed when no listener is installed", () => {
22
+ const target = new EventTarget();
23
+ assert.equal(dispatchSessionRowContextMenu(createDetail(), target), false);
24
+ });
25
+
26
+ test("delivers the stable session-row detail without requiring a listener to claim it", () => {
27
+ const target = new EventTarget();
28
+ const detail = createDetail();
29
+ let received;
30
+ target.addEventListener(SESSION_ROW_CONTEXT_MENU_EVENT, (event) => {
31
+ received = event;
32
+ });
33
+
34
+ assert.equal(dispatchSessionRowContextMenu(detail, target), false);
35
+ assert.equal(received.cancelable, true);
36
+ assert.equal(received.detail, detail);
37
+ });
38
+
39
+ test("reports the menu as handled when a listener cancels the extension event", () => {
40
+ const target = new EventTarget();
41
+ target.addEventListener(SESSION_ROW_CONTEXT_MENU_EVENT, (event) => {
42
+ event.preventDefault();
43
+ });
44
+
45
+ assert.equal(dispatchSessionRowContextMenu(createDetail(), target), true);
46
+ });
@@ -0,0 +1,29 @@
1
+ export const SESSION_ROW_CONTEXT_MENU_EVENT = "pi-web:session-row-contextmenu";
2
+
3
+ export interface SessionRowContextMenuDetail {
4
+ id: string;
5
+ path: string;
6
+ cwd: string;
7
+ name?: string;
8
+ clientX: number;
9
+ clientY: number;
10
+ refresh: () => void;
11
+ }
12
+
13
+ declare global {
14
+ interface WindowEventMap {
15
+ "pi-web:session-row-contextmenu": CustomEvent<SessionRowContextMenuDetail>;
16
+ }
17
+ }
18
+
19
+ /** Returns true when a synchronous listener claims the context menu. */
20
+ export function dispatchSessionRowContextMenu(
21
+ detail: SessionRowContextMenuDetail,
22
+ target: EventTarget = window,
23
+ ): boolean {
24
+ const event = new CustomEvent<SessionRowContextMenuDetail>(SESSION_ROW_CONTEXT_MENU_EVENT, {
25
+ cancelable: true,
26
+ detail,
27
+ });
28
+ return !target.dispatchEvent(event);
29
+ }