@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,1703 @@
1
+ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
+ import { createAgentSessionFromServices, createAgentSessionServices, getAgentDir, initTheme, SessionManager, SettingsManager, Theme } from "@earendil-works/pi-coding-agent";
3
+ import { KeybindingsManager as TuiKeybindingsManager, TUI_KEYBINDINGS } from "@earendil-works/pi-tui";
4
+ import { randomUUID } from "crypto";
5
+ import { existsSync, realpathSync, writeFileSync } from "fs";
6
+ import { resolve } from "path";
7
+ import { validateAgentImages } from "./image-attachments";
8
+ import { invalidateModelsCache } from "./models-cache";
9
+ import { resolveVisibleModels, selectInitialModelScope } from "./model-scope";
10
+ import {
11
+ createProjectCommandBashExtension,
12
+ createProjectCommandBashOperations,
13
+ preferUserBashExtension,
14
+ } from "./project-command-env";
15
+ import { cacheSessionPath, invalidateSessionListCache } from "./session-reader";
16
+ import { getProjectTrustStatus, projectTrustReloadOptions } from "./project-trust";
17
+ import { persistExplicitStartupPreferences } from "./startup-preferences";
18
+ import type { SlashCommandInfo } from "@earendil-works/pi-coding-agent";
19
+ import type { AgentSessionLike, ExtensionUiContextLike, ToolInfo } from "./pi-types";
20
+ import type { RunningPhase, RunningSessionState } from "./board-types";
21
+ import type {
22
+ ExtensionUiRequest,
23
+ ExtensionUiResponse,
24
+ ExtensionWidgetItem,
25
+ SessionInfo,
26
+ SessionMessageEntry,
27
+ } from "./types";
28
+ import { createHeadlessCustomUiTui, DEFAULT_CUSTOM_UI_COLUMNS, type HeadlessCustomUiTui } from "./custom-ui-terminal";
29
+
30
+ // ============================================================================
31
+ // Types
32
+ // ============================================================================
33
+
34
+ export interface AgentEvent {
35
+ type: string;
36
+ [key: string]: unknown;
37
+ }
38
+
39
+ type EventListener = (event: AgentEvent) => void;
40
+
41
+ type PendingUiResponse = {
42
+ resolve: (response: ExtensionUiResponse) => void;
43
+ cancel: () => void;
44
+ };
45
+
46
+ type CustomUiComponent = {
47
+ render: (width: number) => string[];
48
+ handleInput?: (data: string) => void;
49
+ dispose?: () => void;
50
+ invalidate?: () => void;
51
+ };
52
+
53
+ type ExtensionWidgetComponent = {
54
+ render: (width: number) => unknown;
55
+ dispose?: () => void;
56
+ };
57
+
58
+ type ExtensionWidgetFactory = (tui: HeadlessCustomUiTui, theme: Theme) => unknown;
59
+
60
+ type ActiveExtensionWidget = {
61
+ key: string;
62
+ component: ExtensionWidgetComponent;
63
+ placement: "aboveEditor" | "belowEditor";
64
+ generation: number;
65
+ clearEmitted: boolean;
66
+ rendered: boolean;
67
+ };
68
+
69
+ type ActiveCustomUi = {
70
+ component: CustomUiComponent;
71
+ width: number;
72
+ resolve: (value: unknown) => void;
73
+ settled: boolean;
74
+ };
75
+
76
+ type ExtensionUiRequestBody = Record<string, unknown> & {
77
+ method: ExtensionUiRequest["method"];
78
+ timeout?: number;
79
+ expiresAt?: number;
80
+ };
81
+
82
+ type ExtensionCommandContextActionsLike = {
83
+ waitForIdle: () => Promise<void>;
84
+ newSession: () => Promise<{ cancelled: boolean }>;
85
+ fork: () => Promise<{ cancelled: boolean }>;
86
+ navigateTree: (targetId: string, options?: { summarize?: boolean }) => Promise<{ cancelled: boolean }>;
87
+ switchSession: () => Promise<{ cancelled: boolean }>;
88
+ reload: () => Promise<void>;
89
+ };
90
+
91
+ type ExtensionBindingOptions = {
92
+ forceEmptySystemPrompt?: boolean;
93
+ };
94
+
95
+ const IDLE_RESET_EVENT_TYPES = new Set([
96
+ "agent_end",
97
+ "agent_settled",
98
+ "auto_compaction_end",
99
+ "compaction_end",
100
+ ]);
101
+
102
+ export interface RpcSessionStartOptions {
103
+ toolNames?: string[];
104
+ initialModel?: { provider: string; modelId: string };
105
+ thinkingLevel?: ThinkingLevel;
106
+ }
107
+
108
+ const CODING_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls"];
109
+
110
+ // Extensions require a complete Theme, while the web UI applies its own styling.
111
+ class PlainTextTheme extends Theme {
112
+ constructor() {
113
+ super(
114
+ { thinkingXhigh: "", searchMatchText: "" } as ConstructorParameters<typeof Theme>[0],
115
+ { selectedBg: "" } as ConstructorParameters<typeof Theme>[1],
116
+ "truecolor",
117
+ );
118
+ }
119
+
120
+ override fg(...[, text]: Parameters<Theme["fg"]>): string { return text; }
121
+ override bg(...[, text]: Parameters<Theme["bg"]>): string { return text; }
122
+ override bold(text: string): string { return text; }
123
+ override italic(text: string): string { return text; }
124
+ override underline(text: string): string { return text; }
125
+ override inverse(text: string): string { return text; }
126
+ override strikethrough(text: string): string { return text; }
127
+ override getFgAnsi(): string { return ""; }
128
+ override getBgAnsi(): string { return ""; }
129
+ override getThinkingBorderColor(): (text: string) => string {
130
+ return (text) => text;
131
+ }
132
+ override getBashModeBorderColor(): (text: string) => string { return (text) => text; }
133
+ }
134
+
135
+ const PLAIN_TEXT_THEME = new PlainTextTheme();
136
+ const CUSTOM_UI_KEYBINDINGS = new TuiKeybindingsManager(TUI_KEYBINDINGS);
137
+
138
+ function withExtensionTools(session: AgentSessionLike, toolNames: string[]): string[] {
139
+ if (toolNames.length === 0) return [];
140
+
141
+ const codingToolNames = new Set(CODING_TOOL_NAMES);
142
+ const extensionToolNames = session
143
+ .getAllTools()
144
+ .map((t) => t.name)
145
+ .filter((name) => !codingToolNames.has(name));
146
+
147
+ return [...new Set([...toolNames, ...extensionToolNames])];
148
+ }
149
+
150
+ // ============================================================================
151
+ // AgentSessionWrapper
152
+ // Wraps AgentSession with the same interface the rest of the app expects
153
+ // ============================================================================
154
+
155
+ export class AgentSessionWrapper {
156
+ private listeners: EventListener[] = [];
157
+ private pendingUiResponses = new Map<string, PendingUiResponse>();
158
+ private pendingUiRequests = new Map<string, AgentEvent>();
159
+ private activeCustomUis = new Map<string, ActiveCustomUi>();
160
+ private extensionStatuses = new Map<string, string>();
161
+ private extensionWidgets = new Map<string, ExtensionWidgetItem>();
162
+ private activeExtensionWidgets = new Map<string, ActiveExtensionWidget>();
163
+ private extensionWidgetGenerations = new Map<string, number>();
164
+ private extensionWidgetsResetting = false;
165
+ private pendingPromptCount = 0;
166
+ private promptAdmissionTail: Promise<void> = Promise.resolve();
167
+ private extensionsBound = false;
168
+ private extensionBindingPromise: Promise<void> | null = null;
169
+ private extensionBindingError: unknown = null;
170
+ private forceEmptySystemPrompt = false;
171
+ private unsubscribe: (() => void) | null = null;
172
+ private idleTimer: ReturnType<typeof setTimeout> | null = null;
173
+ private onDestroyCallback: (() => void) | null = null;
174
+ private shutdownPromise: Promise<void> | null = null;
175
+ private _alive = true;
176
+ /** 最近一次 agent_start 的时间(看板“运行时长”用);0 = 未知 */
177
+ private lastAgentStart = 0;
178
+
179
+ constructor(public readonly inner: AgentSessionLike) {}
180
+
181
+ /** 是否有等待用户响应的扩展 UI 请求(waiting_input 判定用) */
182
+ hasPendingUiRequest(): boolean {
183
+ return this.pendingUiRequests.size > 0;
184
+ }
185
+
186
+ /** 最近一次 agent 运行开始时间(ms epoch),无则 0 */
187
+ lastAgentStartAt(): number {
188
+ return this.lastAgentStart;
189
+ }
190
+
191
+ get sessionId(): string {
192
+ return this.inner.sessionId;
193
+ }
194
+
195
+ get sessionFile(): string {
196
+ return this.inner.sessionFile ?? "";
197
+ }
198
+
199
+ get cwd(): string {
200
+ return this.inner.sessionManager.getCwd();
201
+ }
202
+
203
+ get streamingMessage() {
204
+ return this.inner.agent.state?.streamingMessage;
205
+ }
206
+
207
+ get isStreaming(): boolean {
208
+ return this.inner.isStreaming;
209
+ }
210
+
211
+ isAlive(): boolean {
212
+ return this._alive;
213
+ }
214
+
215
+ isRunning(): boolean {
216
+ return this._alive && (this.pendingPromptCount > 0 || this.inner.isStreaming || this.inner.isCompacting || this.inner.isBashRunning);
217
+ }
218
+
219
+ start(): void {
220
+ this.unsubscribe = this.inner.subscribe((event: AgentEvent) => {
221
+ if (event.type === "agent_start") {
222
+ this.lastAgentStart = Date.now();
223
+ }
224
+ if (event.type === "agent_end") {
225
+ invalidateSessionListCache();
226
+ }
227
+ if (IDLE_RESET_EVENT_TYPES.has(event.type)) this.resetIdleTimer();
228
+ this.emit(event);
229
+ });
230
+ this.resetIdleTimer();
231
+ }
232
+
233
+ setForceEmptySystemPrompt(force: boolean): void {
234
+ this.forceEmptySystemPrompt = force;
235
+ this.applyForcedEmptySystemPrompt();
236
+ }
237
+
238
+ beginExtensionBinding(options: ExtensionBindingOptions = {}): void {
239
+ void this.ensureExtensionsBound(options).catch((err) => {
240
+ console.error("[pi-web] failed to dispatch session_start to extensions:", err instanceof Error ? err.message : err);
241
+ });
242
+ }
243
+
244
+ async waitUntilReady(): Promise<void> {
245
+ await this.waitForExtensionsBound();
246
+ }
247
+
248
+ private ensureExtensionsBound(options: ExtensionBindingOptions = {}): Promise<void> {
249
+ if (options.forceEmptySystemPrompt) this.forceEmptySystemPrompt = true;
250
+ if (this.extensionsBound) {
251
+ this.applyForcedEmptySystemPrompt();
252
+ return Promise.resolve();
253
+ }
254
+ if (this.extensionBindingPromise) return this.extensionBindingPromise;
255
+
256
+ this.extensionBindingError = null;
257
+ this.extensionBindingPromise = (async () => {
258
+ if (!this._alive) return;
259
+ const uiContext = this.createExtensionUiContext();
260
+ if (typeof this.inner.bindExtensions === "function") {
261
+ const bindExtensions = this.inner.bindExtensions as (bindings: {
262
+ uiContext?: ExtensionUiContextLike;
263
+ mode?: "rpc";
264
+ commandContextActions?: ExtensionCommandContextActionsLike;
265
+ shutdownHandler?: () => void;
266
+ onError?: (error: { extensionPath: string; event: string; error: string }) => void;
267
+ }) => Promise<void>;
268
+ await bindExtensions.call(this.inner, {
269
+ uiContext,
270
+ mode: "rpc",
271
+ commandContextActions: this.createExtensionCommandContextActions(),
272
+ shutdownHandler: () => this.emit({
273
+ type: "extension_ui_request",
274
+ id: randomUUID(),
275
+ method: "notify",
276
+ notifyType: "warning",
277
+ message: "Extension requested shutdown, but shutdown is not supported in Pi Web.",
278
+ } as ExtensionUiRequest as AgentEvent),
279
+ onError: (error) => this.emit({
280
+ type: "extension_error",
281
+ extensionPath: error.extensionPath,
282
+ event: error.event,
283
+ error: error.error,
284
+ }),
285
+ });
286
+ } else {
287
+ this.inner.extensionRunner.setUIContext?.(uiContext, "rpc");
288
+ }
289
+ this.extensionsBound = true;
290
+ this.applyForcedEmptySystemPrompt();
291
+ console.log(`[pi-web] session_start dispatched to extensions for session ${this.inner.sessionId}`);
292
+ })().catch((err) => {
293
+ this.extensionBindingError = err;
294
+ throw err;
295
+ });
296
+
297
+ return this.extensionBindingPromise;
298
+ }
299
+
300
+ private async waitForExtensionsBound(): Promise<void> {
301
+ try {
302
+ if (this.extensionBindingPromise) await this.extensionBindingPromise;
303
+ } catch (err) {
304
+ throw err instanceof Error ? err : new Error(String(err));
305
+ }
306
+ if (this.extensionBindingError) {
307
+ throw this.extensionBindingError instanceof Error
308
+ ? this.extensionBindingError
309
+ : new Error(String(this.extensionBindingError));
310
+ }
311
+ }
312
+
313
+ private shouldWaitForExtensions(type: string): boolean {
314
+ return type === "prompt"
315
+ || type === "steer"
316
+ || type === "follow_up"
317
+ || type === "get_commands"
318
+ || type === "get_state";
319
+ }
320
+
321
+ private async withFinalRunningNotification<T>(operation: () => Promise<T>): Promise<T> {
322
+ try {
323
+ return await operation();
324
+ } finally {
325
+ this.resetIdleTimer();
326
+ }
327
+ }
328
+
329
+ private applyForcedEmptySystemPrompt(): void {
330
+ if (this.forceEmptySystemPrompt && this.inner.agent.state) {
331
+ this.inner.agent.state.systemPrompt = "";
332
+ }
333
+ }
334
+
335
+ private emit(event: AgentEvent): void {
336
+ for (const listener of this.listeners) {
337
+ try {
338
+ listener(event);
339
+ } catch (error) {
340
+ console.error(
341
+ `[pi-web] failed to deliver ${event.type} event:`,
342
+ error instanceof Error ? error.message : error,
343
+ );
344
+ }
345
+ }
346
+ }
347
+
348
+ private async acquirePromptAdmission(): Promise<() => void> {
349
+ const previous = this.promptAdmissionTail;
350
+ let release!: () => void;
351
+ this.promptAdmissionTail = new Promise<void>((resolve) => {
352
+ release = resolve;
353
+ });
354
+ await previous;
355
+ return release;
356
+ }
357
+
358
+ private resetIdleTimer(): void {
359
+ if (this.idleTimer) clearTimeout(this.idleTimer);
360
+ this.idleTimer = setTimeout(() => {
361
+ if (this.isRunning()) {
362
+ this.resetIdleTimer();
363
+ return;
364
+ }
365
+ void this.shutdown().catch((error) => {
366
+ console.error("[pi-web] failed to shut down idle session:", error instanceof Error ? error.message : error);
367
+ });
368
+ }, 10 * 60 * 1000);
369
+ }
370
+
371
+ onEvent(listener: EventListener): () => void {
372
+ this.listeners.push(listener);
373
+ for (const event of this.pendingUiRequests.values()) listener(event);
374
+ return () => {
375
+ const i = this.listeners.indexOf(listener);
376
+ if (i !== -1) this.listeners.splice(i, 1);
377
+ };
378
+ }
379
+
380
+ onDestroy(cb: () => void): void {
381
+ this.onDestroyCallback = cb;
382
+ }
383
+
384
+ async send(command: Record<string, unknown>): Promise<unknown> {
385
+ this.resetIdleTimer();
386
+ const type = command.type as string;
387
+ if (this.shouldWaitForExtensions(type)) await this.waitForExtensionsBound();
388
+
389
+ if (type === "prompt" || type === "steer" || type === "follow_up") {
390
+ const imageError = validateAgentImages(command.images);
391
+ if (imageError) throw new Error(imageError);
392
+ }
393
+
394
+ switch (type) {
395
+ case "prompt": {
396
+ // Serialize only admission. Once the preceding prompt has either
397
+ // passed or failed preflight, the SDK can atomically decide whether
398
+ // this submission starts a run or joins its streaming queue.
399
+ const releaseAdmission = await this.acquirePromptAdmission();
400
+ try {
401
+ if (this.inner.isBashRunning) {
402
+ throw new Error("Cannot send a prompt while a shell command is running");
403
+ }
404
+ const promptImages = command.images as Array<{ type: "image"; data: string; mimeType: string }> | undefined;
405
+ const streamingBehavior = command.streamingBehavior as "steer" | "followUp" | undefined;
406
+ let preflightAccepted = false;
407
+ let preflightSettled = false;
408
+ let promptSettled = false;
409
+ let acceptPreflight!: () => void;
410
+ let rejectPreflight!: (error: unknown) => void;
411
+ const preflight = new Promise<void>((resolve, reject) => {
412
+ acceptPreflight = () => {
413
+ preflightAccepted = true;
414
+ if (preflightSettled) return;
415
+ preflightSettled = true;
416
+ resolve();
417
+ };
418
+ rejectPreflight = (error) => {
419
+ if (preflightSettled) return;
420
+ preflightSettled = true;
421
+ reject(error);
422
+ };
423
+ });
424
+ const finishPrompt = () => {
425
+ if (promptSettled) return;
426
+ promptSettled = true;
427
+ this.pendingPromptCount = Math.max(0, this.pendingPromptCount - 1);
428
+ this.resetIdleTimer();
429
+ };
430
+
431
+ this.pendingPromptCount += 1;
432
+ let prompt: Promise<void>;
433
+ try {
434
+ prompt = this.inner.prompt(command.message as string, {
435
+ ...(promptImages?.length ? { images: promptImages } : {}),
436
+ ...(streamingBehavior ? { streamingBehavior } : {}),
437
+ source: "rpc",
438
+ // Match pi's RPC contract: acknowledge only after synchronous prompt
439
+ // validation and extension preflight have accepted the submission.
440
+ preflightResult: (success) => {
441
+ if (success) acceptPreflight();
442
+ },
443
+ });
444
+ } catch (error) {
445
+ finishPrompt();
446
+ throw error;
447
+ }
448
+
449
+ void prompt.then(() => {
450
+ // Compatibility fallback if a future SDK resolves without invoking
451
+ // the internal callback. This waits for the run, but never acks early.
452
+ acceptPreflight();
453
+ finishPrompt();
454
+ if (!streamingBehavior) this.emit({ type: "prompt_done" });
455
+ }, (error) => {
456
+ rejectPreflight(error);
457
+ finishPrompt();
458
+ invalidateSessionListCache();
459
+ // A preflight rejection is returned by the POST itself. Only an
460
+ // unexpected failure after acceptance needs the asynchronous event.
461
+ if (preflightAccepted) {
462
+ this.emit({
463
+ type: "prompt_error",
464
+ errorMessage: error instanceof Error ? error.message : String(error),
465
+ });
466
+ if (!streamingBehavior) this.emit({ type: "prompt_done" });
467
+ }
468
+ }).catch((error) => {
469
+ console.error(
470
+ "[pi-web] prompt completion handler failed:",
471
+ error instanceof Error ? error.message : error,
472
+ );
473
+ });
474
+
475
+ await preflight;
476
+ return null;
477
+ } finally {
478
+ releaseAdmission();
479
+ }
480
+ }
481
+
482
+ case "abort":
483
+ await this.withFinalRunningNotification(() => this.inner.abort());
484
+ return null;
485
+
486
+ case "get_state": {
487
+ const model = this.inner.model;
488
+ const contextUsage = this.inner.getContextUsage();
489
+ return {
490
+ sessionId: this.inner.sessionId,
491
+ sessionFile: this.inner.sessionFile ?? "",
492
+ isStreaming: this.inner.isStreaming,
493
+ isPromptRunning: this.pendingPromptCount > 0,
494
+ isBashRunning: this.inner.isBashRunning,
495
+ isCompacting: this.inner.isCompacting,
496
+ autoCompactionEnabled: this.inner.autoCompactionEnabled,
497
+ autoRetryEnabled: this.inner.autoRetryEnabled,
498
+ model: model ? { id: model.id, provider: model.provider } : undefined,
499
+ messageCount: 0,
500
+ pendingMessageCount: this.inner.pendingMessageCount,
501
+ queuedMessages: {
502
+ steering: [...this.inner.getSteeringMessages()],
503
+ followUp: [...this.inner.getFollowUpMessages()],
504
+ },
505
+ contextUsage: contextUsage
506
+ ? { percent: contextUsage.percent, contextWindow: contextUsage.contextWindow, tokens: contextUsage.tokens }
507
+ : null,
508
+ systemPrompt: this.inner.agent.state?.systemPrompt ?? "",
509
+ thinkingLevel: this.inner.agent.state?.thinkingLevel ?? "off",
510
+ extensionStatuses: this.getExtensionStatuses(),
511
+ extensionWidgets: this.getExtensionWidgets(),
512
+ };
513
+ }
514
+
515
+ case "set_model": {
516
+ const { provider, modelId } = command as { provider: string; modelId: string };
517
+ let model = this.inner.modelRuntime.getModel(provider, modelId);
518
+ if (!model) {
519
+ await this.inner.modelRuntime.refresh({ allowNetwork: false });
520
+ model = this.inner.modelRuntime.getModel(provider, modelId);
521
+ }
522
+ if (!model) throw new Error(`Model not found: ${provider}/${modelId}`);
523
+ await this.inner.setModel(model);
524
+ invalidateModelsCache();
525
+ invalidateSessionListCache();
526
+ return { id: model.id, provider: model.provider };
527
+ }
528
+
529
+ case "fork": {
530
+ if (this.inner.isBashRunning) {
531
+ throw new Error("Cannot fork while a shell command is running");
532
+ }
533
+ const entryId = command.entryId as string;
534
+ const sessionManager = this.inner.sessionManager;
535
+ const currentSessionFile = this.inner.sessionFile;
536
+
537
+ if (!sessionManager.isPersisted()) return { cancelled: true };
538
+ if (!currentSessionFile) throw new Error("Persisted session is missing a session file");
539
+
540
+ const entry = sessionManager.getEntry(entryId);
541
+ if (!entry) throw new Error("Invalid entry ID for forking");
542
+
543
+ const sessionDir = sessionManager.getSessionDir();
544
+ let newSessionFile: string;
545
+
546
+ if (!entry.parentId) {
547
+ // Fork before the first message: create an empty session linked to this one.
548
+ // SessionManager.create + newSession only reserve the in-memory path — the
549
+ // file is materialized lazily on the first assistant message. pi keeps the
550
+ // in-memory manager alive for this, but pi-web reads sessions from disk, so
551
+ // persist the empty header now or the returned session resolves nowhere.
552
+ const newManager = SessionManager.create(sessionManager.getCwd(), sessionDir);
553
+ newManager.newSession({ parentSession: currentSessionFile });
554
+ if (newManager.getSessionFile()) {
555
+ (newManager as unknown as { _rewriteFile(): void })._rewriteFile();
556
+ }
557
+ newSessionFile = newManager.getSessionFile() as string;
558
+ } else {
559
+ // Fork after some history: copy path up to (but not including) the fork point
560
+ const sourceManager = SessionManager.open(currentSessionFile, sessionDir);
561
+ const forkedPath = sourceManager.createBranchedSession(entry.parentId);
562
+ if (!forkedPath) throw new Error("Failed to create forked session");
563
+ newSessionFile = forkedPath;
564
+ }
565
+
566
+ const newSessionId = SessionManager.open(newSessionFile, sessionDir).getSessionId();
567
+ cacheSessionPath(newSessionId, newSessionFile);
568
+ invalidateSessionListCache();
569
+ await this.shutdown();
570
+ return { cancelled: false, newSessionId };
571
+ }
572
+
573
+ case "navigate_tree": {
574
+ if (this.inner.isBashRunning) {
575
+ throw new Error("Cannot navigate while a shell command is running");
576
+ }
577
+ const result = await this.inner.navigateTree(command.targetId as string, {});
578
+ return { cancelled: result.cancelled };
579
+ }
580
+
581
+ case "set_thinking_level": {
582
+ const level = command.level as string;
583
+ this.inner.setThinkingLevel(level);
584
+ // setThinkingLevel clamps xhigh→high for models where supportsXhigh()===false.
585
+ // If the model has DeepSeek thinking compat (reasoningEffortMap maps xhigh→max),
586
+ // force the state back so the compat layer can use it correctly.
587
+ if (level === "xhigh" && (this.inner.model as { compat?: { thinkingFormat?: string } } | null)?.compat?.thinkingFormat === "deepseek" && this.inner.agent?.state) {
588
+ this.inner.agent.state.thinkingLevel = "xhigh";
589
+ }
590
+ invalidateSessionListCache();
591
+ return null;
592
+ }
593
+
594
+ case "compact": {
595
+ try {
596
+ return await this.withFinalRunningNotification(() =>
597
+ this.inner.compact(command.customInstructions as string | undefined)
598
+ );
599
+ } finally {
600
+ invalidateSessionListCache();
601
+ }
602
+ }
603
+
604
+ case "set_session_name": {
605
+ const name = (command.name as string | undefined)?.trim();
606
+ if (!name) throw new Error("Session name cannot be empty");
607
+ this.inner.setSessionName(name);
608
+ invalidateSessionListCache();
609
+ return null;
610
+ }
611
+
612
+ case "get_session_stats": {
613
+ return {
614
+ ...this.inner.getSessionStats(),
615
+ sessionName: this.inner.sessionManager.getSessionName(),
616
+ };
617
+ }
618
+
619
+ case "get_last_assistant_text": {
620
+ return { text: this.inner.getLastAssistantText() ?? "" };
621
+ }
622
+
623
+ case "set_auto_compaction": {
624
+ this.inner.setAutoCompactionEnabled(command.enabled as boolean);
625
+ return null;
626
+ }
627
+
628
+ case "clear_queue": {
629
+ // Full clear only: pi has no single-item dequeue, and clear+requeue
630
+ // races against the agent loop pulling messages mid-flight.
631
+ return this.inner.clearQueue();
632
+ }
633
+
634
+ case "steer": {
635
+ const steerImages = command.images as Array<{ type: "image"; data: string; mimeType: string }> | undefined;
636
+ await this.inner.steer(command.message as string, steerImages?.length ? steerImages : undefined);
637
+ return null;
638
+ }
639
+
640
+ case "follow_up": {
641
+ const followImages = command.images as Array<{ type: "image"; data: string; mimeType: string }> | undefined;
642
+ await this.inner.followUp(command.message as string, followImages?.length ? followImages : undefined);
643
+ return null;
644
+ }
645
+
646
+ case "get_tools": {
647
+ const all: ToolInfo[] = this.inner.getAllTools();
648
+ const active = new Set<string>(this.inner.getActiveToolNames());
649
+ return all.map((t) => ({
650
+ name: t.name,
651
+ description: t.description,
652
+ active: active.has(t.name),
653
+ }));
654
+ }
655
+
656
+ case "get_commands": {
657
+ const commands: SlashCommandInfo[] = [];
658
+ for (const registered of this.inner.extensionRunner.getRegisteredCommands()) {
659
+ commands.push({
660
+ name: registered.invocationName,
661
+ description: registered.description,
662
+ source: "extension",
663
+ sourceInfo: registered.sourceInfo,
664
+ });
665
+ }
666
+ for (const template of this.inner.promptTemplates) {
667
+ commands.push({
668
+ name: template.name,
669
+ description: template.description,
670
+ source: "prompt",
671
+ sourceInfo: template.sourceInfo,
672
+ });
673
+ }
674
+ for (const skill of this.inner.resourceLoader.getSkills().skills) {
675
+ commands.push({
676
+ name: `skill:${skill.name}`,
677
+ description: skill.description,
678
+ source: "skill",
679
+ sourceInfo: skill.sourceInfo,
680
+ });
681
+ }
682
+ return { commands };
683
+ }
684
+
685
+ case "set_tools": {
686
+ const toolNames = command.toolNames as string[];
687
+ this.setForceEmptySystemPrompt(toolNames.length === 0);
688
+ this.inner.setActiveToolsByName(withExtensionTools(this.inner, toolNames));
689
+ this.applyForcedEmptySystemPrompt();
690
+ return null;
691
+ }
692
+
693
+ case "reload": {
694
+ await this.waitForExtensionsBound();
695
+ this.extensionStatuses.clear();
696
+ this.resetExtensionWidgetsForReload();
697
+ this.syncProjectTrust();
698
+ await this.inner.reload();
699
+ if (typeof this.inner.bindExtensions !== "function") {
700
+ this.inner.extensionRunner.setUIContext?.(this.createExtensionUiContext(), "rpc");
701
+ }
702
+ this.applyForcedEmptySystemPrompt();
703
+ invalidateModelsCache();
704
+ return { success: true };
705
+ }
706
+
707
+ case "abort_compaction": {
708
+ this.inner.abortCompaction();
709
+ return null;
710
+ }
711
+
712
+ case "extension_ui_response": {
713
+ this.resolveExtensionUiResponse(command as ExtensionUiResponse);
714
+ return null;
715
+ }
716
+
717
+ case "extension_ui_input": {
718
+ this.handleExtensionUiInput(command.id as string, command.data as string);
719
+ return null;
720
+ }
721
+
722
+ case "set_auto_retry": {
723
+ this.inner.setAutoRetryEnabled(command.enabled as boolean);
724
+ return null;
725
+ }
726
+
727
+ case "bash": {
728
+ if (this.pendingPromptCount > 0 || this.inner.isStreaming || this.inner.isCompacting || this.inner.isBashRunning) {
729
+ throw new Error("Cannot run a shell command while the session is busy");
730
+ }
731
+ const execution = this.inner.executeBash(
732
+ command.command as string,
733
+ undefined,
734
+ {
735
+ excludeFromContext: command.excludeFromContext as boolean | undefined,
736
+ operations: createProjectCommandBashOperations({
737
+ shellPath: this.inner.settingsManager.getShellPath(),
738
+ }),
739
+ },
740
+ );
741
+ try {
742
+ const result = await execution;
743
+ return result;
744
+ } finally {
745
+ this.resetIdleTimer();
746
+ invalidateSessionListCache();
747
+ }
748
+ }
749
+
750
+ case "abort_bash": {
751
+ this.inner.abortBash();
752
+ return null;
753
+ }
754
+
755
+ default:
756
+ throw new Error(`Unsupported command: ${type}`);
757
+ }
758
+ }
759
+
760
+ destroy(): void {
761
+ if (!this._alive) return;
762
+ this._alive = false;
763
+ if (this.idleTimer) clearTimeout(this.idleTimer);
764
+ if (this.inner.isBashRunning) this.inner.abortBash();
765
+ this.unsubscribe?.();
766
+ for (const pending of this.pendingUiResponses.values()) pending.cancel();
767
+ for (const id of Array.from(this.activeCustomUis.keys())) this.closeCustomUi(id, undefined);
768
+ this.pendingUiResponses.clear();
769
+ this.pendingUiRequests.clear();
770
+ this.clearExtensionWidgets(false);
771
+ try {
772
+ this.inner.dispose();
773
+ } finally {
774
+ this.onDestroyCallback?.();
775
+ }
776
+ }
777
+
778
+ async shutdown(): Promise<void> {
779
+ if (this.shutdownPromise) return this.shutdownPromise;
780
+ if (!this._alive) return;
781
+
782
+ this.shutdownPromise = (async () => {
783
+ try {
784
+ try {
785
+ await this.waitForExtensionsBound();
786
+ } catch (error) {
787
+ console.error(
788
+ "[pi-web] extension binding failed before session shutdown:",
789
+ error instanceof Error ? error.message : error,
790
+ );
791
+ }
792
+ await this.inner.extensionRunner.emit?.({ type: "session_shutdown", reason: "quit" });
793
+ } finally {
794
+ this.destroy();
795
+ }
796
+ })();
797
+ return this.shutdownPromise;
798
+ }
799
+
800
+ private resolveExtensionUiResponse(response: ExtensionUiResponse): void {
801
+ const pending = this.pendingUiResponses.get(response.id);
802
+ if (!pending) return;
803
+ pending.resolve(response);
804
+ }
805
+
806
+ private getExtensionStatuses(): Array<{ key: string; text: string }> {
807
+ return Array.from(this.extensionStatuses, ([key, text]) => ({ key, text }));
808
+ }
809
+
810
+ private getExtensionWidgets(): ExtensionWidgetItem[] {
811
+ return Array.from(this.extensionWidgets.values());
812
+ }
813
+
814
+ private nextExtensionWidgetGeneration(key: string): number {
815
+ const generation = (this.extensionWidgetGenerations.get(key) ?? 0) + 1;
816
+ this.extensionWidgetGenerations.set(key, generation);
817
+ return generation;
818
+ }
819
+
820
+ private disposeExtensionWidgetComponent(component: unknown): void {
821
+ if (!component || (typeof component !== "object" && typeof component !== "function")) return;
822
+ const dispose = (component as { dispose?: unknown }).dispose;
823
+ if (typeof dispose !== "function") return;
824
+ try {
825
+ dispose.call(component);
826
+ } catch {
827
+ // Ignore dispose errors from extension widgets.
828
+ }
829
+ }
830
+
831
+ private emitExtensionWidgetClear(key: string): void {
832
+ this.emit({
833
+ type: "extension_ui_request",
834
+ id: randomUUID(),
835
+ method: "setWidget",
836
+ widgetKey: key,
837
+ widgetLines: undefined,
838
+ widgetPlacement: undefined,
839
+ } as ExtensionUiRequest as AgentEvent);
840
+ }
841
+
842
+ private clearExtensionWidget(key: string, emitClear = true): number {
843
+ const generation = this.nextExtensionWidgetGeneration(key);
844
+
845
+ const active = this.activeExtensionWidgets.get(key);
846
+ this.activeExtensionWidgets.delete(key);
847
+ this.extensionWidgets.delete(key);
848
+ if (active) this.disposeExtensionWidgetComponent(active.component);
849
+ if (this.extensionWidgetGenerations.get(key) !== generation) return generation;
850
+ if (emitClear) this.emitExtensionWidgetClear(key);
851
+ return generation;
852
+ }
853
+
854
+ private clearExtensionWidgets(emitClear: boolean): void {
855
+ const keys = new Set([
856
+ ...this.extensionWidgets.keys(),
857
+ ...this.activeExtensionWidgets.keys(),
858
+ ]);
859
+ for (const key of keys) this.clearExtensionWidget(key, emitClear);
860
+ }
861
+
862
+ private resetExtensionWidgetsForReload(): void {
863
+ this.extensionWidgetsResetting = true;
864
+ try {
865
+ const factoryKeys = [...this.activeExtensionWidgets.keys()];
866
+ for (const key of factoryKeys) this.clearExtensionWidget(key);
867
+ // Keep the existing array-widget reload behavior: snapshots are reset and
868
+ // the next extension session_start repopulates them.
869
+ this.extensionWidgets.clear();
870
+ } finally {
871
+ this.extensionWidgetsResetting = false;
872
+ }
873
+ }
874
+
875
+ private emitExtensionWidgetError(key: string, error: unknown): void {
876
+ this.emit({
877
+ type: "extension_error",
878
+ extensionPath: `extension-widget:${key}`,
879
+ event: "setWidget",
880
+ error: error instanceof Error ? error.message : String(error),
881
+ });
882
+ }
883
+
884
+ private failExtensionWidget(
885
+ key: string,
886
+ generation: number,
887
+ error: unknown,
888
+ clearEmitted: boolean,
889
+ component?: unknown,
890
+ ): void {
891
+ if (this.extensionWidgetGenerations.get(key) !== generation) {
892
+ this.disposeExtensionWidgetComponent(component);
893
+ return;
894
+ }
895
+
896
+ const active = this.activeExtensionWidgets.get(key);
897
+ let shouldEmitClear = !clearEmitted;
898
+ if (active?.generation === generation) {
899
+ shouldEmitClear = active.rendered || !active.clearEmitted;
900
+ this.activeExtensionWidgets.delete(key);
901
+ this.disposeExtensionWidgetComponent(active.component);
902
+ } else {
903
+ this.disposeExtensionWidgetComponent(component);
904
+ }
905
+ if (this.extensionWidgetGenerations.get(key) !== generation) {
906
+ this.emitExtensionWidgetError(key, error);
907
+ return;
908
+ }
909
+ this.extensionWidgets.delete(key);
910
+ if (shouldEmitClear) this.emitExtensionWidgetClear(key);
911
+ this.emitExtensionWidgetError(key, error);
912
+ }
913
+
914
+ private renderExtensionWidget(active: ActiveExtensionWidget): void {
915
+ if (
916
+ this.activeExtensionWidgets.get(active.key) !== active
917
+ || this.extensionWidgetGenerations.get(active.key) !== active.generation
918
+ ) return;
919
+
920
+ let lines: unknown;
921
+ try {
922
+ lines = active.component.render(DEFAULT_CUSTOM_UI_COLUMNS);
923
+ } catch (error) {
924
+ this.failExtensionWidget(active.key, active.generation, error, active.clearEmitted);
925
+ return;
926
+ }
927
+ if (!Array.isArray(lines) || !lines.every((line) => typeof line === "string")) {
928
+ this.failExtensionWidget(
929
+ active.key,
930
+ active.generation,
931
+ new Error("Extension widget render must return string[]"),
932
+ active.clearEmitted,
933
+ );
934
+ return;
935
+ }
936
+ if (
937
+ this.activeExtensionWidgets.get(active.key) !== active
938
+ || this.extensionWidgetGenerations.get(active.key) !== active.generation
939
+ ) return;
940
+
941
+ const widgetLines = lines as string[];
942
+ this.extensionWidgets.set(active.key, {
943
+ key: active.key,
944
+ lines: widgetLines,
945
+ placement: active.placement,
946
+ });
947
+ active.rendered = true;
948
+ this.emit({
949
+ type: "extension_ui_request",
950
+ id: randomUUID(),
951
+ method: "setWidget",
952
+ widgetKey: active.key,
953
+ widgetLines,
954
+ widgetPlacement: active.placement,
955
+ } as ExtensionUiRequest as AgentEvent);
956
+ }
957
+
958
+ private setExtensionWidgetFactory(
959
+ key: string,
960
+ factory: ExtensionWidgetFactory,
961
+ options?: { placement?: "aboveEditor" | "belowEditor" },
962
+ ): void {
963
+ const hadPrevious = this.extensionWidgets.has(key) || this.activeExtensionWidgets.has(key);
964
+ const generation = this.clearExtensionWidget(key, hadPrevious);
965
+ if (this.extensionWidgetGenerations.get(key) !== generation) return;
966
+ const tui = createHeadlessCustomUiTui(() => {
967
+ const active = this.activeExtensionWidgets.get(key);
968
+ if (active?.generation === generation) this.renderExtensionWidget(active);
969
+ }, DEFAULT_CUSTOM_UI_COLUMNS);
970
+
971
+ let component: unknown;
972
+ try {
973
+ component = factory(tui, PLAIN_TEXT_THEME);
974
+ } catch (error) {
975
+ this.failExtensionWidget(key, generation, error, hadPrevious);
976
+ return;
977
+ }
978
+ if (this.extensionWidgetGenerations.get(key) !== generation) {
979
+ this.disposeExtensionWidgetComponent(component);
980
+ return;
981
+ }
982
+ if (
983
+ !component
984
+ || (typeof component !== "object" && typeof component !== "function")
985
+ || typeof (component as { render?: unknown }).render !== "function"
986
+ ) {
987
+ this.failExtensionWidget(
988
+ key,
989
+ generation,
990
+ new Error("Extension widget factory must return a component with render(width)"),
991
+ hadPrevious,
992
+ component,
993
+ );
994
+ return;
995
+ }
996
+
997
+ const active: ActiveExtensionWidget = {
998
+ key,
999
+ component: component as ExtensionWidgetComponent,
1000
+ placement: options?.placement ?? "aboveEditor",
1001
+ generation,
1002
+ clearEmitted: hadPrevious,
1003
+ rendered: false,
1004
+ };
1005
+ this.activeExtensionWidgets.set(key, active);
1006
+ this.renderExtensionWidget(active);
1007
+ }
1008
+
1009
+ private getCustomUiWidth(options: unknown): number {
1010
+ if (!options || typeof options !== "object") return DEFAULT_CUSTOM_UI_COLUMNS;
1011
+ const overlayOptions = (options as { overlayOptions?: unknown }).overlayOptions;
1012
+ const resolved = typeof overlayOptions === "function" ? overlayOptions() : overlayOptions;
1013
+ if (!resolved || typeof resolved !== "object") return DEFAULT_CUSTOM_UI_COLUMNS;
1014
+ const width = (resolved as { width?: unknown }).width;
1015
+ return typeof width === "number" && Number.isFinite(width)
1016
+ ? Math.max(40, Math.min(140, Math.round(width)))
1017
+ : 92;
1018
+ }
1019
+
1020
+ private emitCustomUiRender(id: string, custom: ActiveCustomUi): void {
1021
+ let lines: string[];
1022
+ try {
1023
+ lines = custom.component.render(custom.width);
1024
+ } catch (error) {
1025
+ lines = [`Extension custom UI render failed: ${error instanceof Error ? error.message : String(error)}`];
1026
+ }
1027
+ const event = {
1028
+ type: "extension_ui_request",
1029
+ id,
1030
+ method: "custom",
1031
+ lines,
1032
+ } as ExtensionUiRequest as AgentEvent;
1033
+ this.pendingUiRequests.set(id, event);
1034
+ this.emit(event);
1035
+ }
1036
+
1037
+ private closeCustomUi(id: string, value: unknown): void {
1038
+ const custom = this.activeCustomUis.get(id);
1039
+ if (!custom || custom.settled) return;
1040
+ custom.settled = true;
1041
+ this.activeCustomUis.delete(id);
1042
+ this.pendingUiRequests.delete(id);
1043
+ try {
1044
+ custom.component.dispose?.();
1045
+ } catch {
1046
+ // Ignore dispose errors from extension UI components.
1047
+ }
1048
+ this.emit({
1049
+ type: "extension_ui_request",
1050
+ id,
1051
+ method: "custom",
1052
+ lines: [],
1053
+ closed: true,
1054
+ } as ExtensionUiRequest as AgentEvent);
1055
+ custom.resolve(value);
1056
+ }
1057
+
1058
+ private handleExtensionUiInput(id: string, data: string): void {
1059
+ const custom = this.activeCustomUis.get(id);
1060
+ if (!custom || typeof data !== "string") return;
1061
+ try {
1062
+ custom.component.handleInput?.(data);
1063
+ if (this.activeCustomUis.has(id)) this.emitCustomUiRender(id, custom);
1064
+ } catch (error) {
1065
+ this.closeCustomUi(id, undefined);
1066
+ this.emit({
1067
+ type: "extension_error",
1068
+ extensionPath: `custom-ui:${id}`,
1069
+ event: "custom_ui_input",
1070
+ error: error instanceof Error ? error.message : String(error),
1071
+ });
1072
+ }
1073
+ }
1074
+
1075
+ private requestExtensionCustomUi<T>(
1076
+ factory: unknown,
1077
+ options?: unknown,
1078
+ ): Promise<T> {
1079
+ if (typeof factory !== "function") return Promise.resolve(undefined as T);
1080
+
1081
+ const id = randomUUID();
1082
+ const width = this.getCustomUiWidth(options);
1083
+
1084
+ return new Promise<T>((resolve) => {
1085
+ let completed = false;
1086
+ const tui = createHeadlessCustomUiTui(
1087
+ () => {
1088
+ const custom = this.activeCustomUis.get(id);
1089
+ if (custom) this.emitCustomUiRender(id, custom);
1090
+ },
1091
+ width,
1092
+ );
1093
+ const finish = (value: T) => {
1094
+ if (completed) return;
1095
+ completed = true;
1096
+ resolve(value);
1097
+ };
1098
+ const done = (value: T) => {
1099
+ if (this.activeCustomUis.has(id)) {
1100
+ this.closeCustomUi(id, value);
1101
+ } else {
1102
+ finish(value);
1103
+ }
1104
+ };
1105
+
1106
+ Promise.resolve()
1107
+ .then(() => factory(tui, PLAIN_TEXT_THEME, CUSTOM_UI_KEYBINDINGS, done))
1108
+ .then((component) => {
1109
+ if (completed) {
1110
+ try {
1111
+ (component as CustomUiComponent | undefined)?.dispose?.();
1112
+ } catch {
1113
+ // Ignore dispose errors from a component completed before mounting.
1114
+ }
1115
+ return;
1116
+ }
1117
+ if (!component || typeof component !== "object" || typeof (component as CustomUiComponent).render !== "function") {
1118
+ finish(undefined as T);
1119
+ return;
1120
+ }
1121
+ const custom: ActiveCustomUi = {
1122
+ component: component as CustomUiComponent,
1123
+ width,
1124
+ resolve: (value) => finish(value as T),
1125
+ settled: false,
1126
+ };
1127
+ this.activeCustomUis.set(id, custom);
1128
+ this.emitCustomUiRender(id, custom);
1129
+ })
1130
+ .catch((error) => {
1131
+ if (completed) return;
1132
+ this.emit({
1133
+ type: "extension_error",
1134
+ extensionPath: `custom-ui:${id}`,
1135
+ event: "custom_ui",
1136
+ error: error instanceof Error ? error.message : String(error),
1137
+ });
1138
+ finish(undefined as T);
1139
+ });
1140
+ });
1141
+ }
1142
+
1143
+ private requestExtensionUi<T>(
1144
+ request: ExtensionUiRequestBody,
1145
+ defaultValue: T,
1146
+ parseResponse: (response: ExtensionUiResponse) => T,
1147
+ timeout?: number,
1148
+ signal?: AbortSignal,
1149
+ ): Promise<T> {
1150
+ if (signal?.aborted) return Promise.resolve(defaultValue);
1151
+
1152
+ const id = randomUUID();
1153
+ const fullRequest = {
1154
+ type: "extension_ui_request",
1155
+ id,
1156
+ ...request,
1157
+ ...(timeout ? { timeout, expiresAt: Date.now() + timeout } : {}),
1158
+ };
1159
+
1160
+ return new Promise((resolve) => {
1161
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
1162
+ const cleanup = () => {
1163
+ if (timeoutId) clearTimeout(timeoutId);
1164
+ signal?.removeEventListener("abort", onAbort);
1165
+ this.pendingUiRequests.delete(id);
1166
+ this.pendingUiResponses.delete(id);
1167
+ };
1168
+ const settle = (value: T) => {
1169
+ cleanup();
1170
+ resolve(value);
1171
+ };
1172
+ const onAbort = () => settle(defaultValue);
1173
+
1174
+ if (timeout) timeoutId = setTimeout(() => settle(defaultValue), timeout);
1175
+ signal?.addEventListener("abort", onAbort, { once: true });
1176
+
1177
+ this.pendingUiRequests.set(id, fullRequest as AgentEvent);
1178
+ this.pendingUiResponses.set(id, {
1179
+ resolve: (response) => settle(parseResponse(response)),
1180
+ cancel: () => settle(defaultValue),
1181
+ });
1182
+ this.emit(fullRequest as AgentEvent);
1183
+ });
1184
+ }
1185
+
1186
+ private createExtensionUiContext(): ExtensionUiContextLike {
1187
+ return {
1188
+ select: (title, options, opts) => this.requestExtensionUi(
1189
+ { method: "select", title, options, ...(opts?.timeout ? { timeout: opts.timeout } : {}) },
1190
+ undefined,
1191
+ (response) => "value" in response ? response.value : undefined,
1192
+ opts?.timeout,
1193
+ opts?.signal,
1194
+ ),
1195
+ confirm: (title, message, opts) => this.requestExtensionUi(
1196
+ { method: "confirm", title, message, ...(opts?.timeout ? { timeout: opts.timeout } : {}) },
1197
+ false,
1198
+ (response) => "confirmed" in response ? response.confirmed : false,
1199
+ opts?.timeout,
1200
+ opts?.signal,
1201
+ ),
1202
+ input: (title, placeholder, opts) => this.requestExtensionUi(
1203
+ { method: "input", title, ...(placeholder !== undefined ? { placeholder } : {}), ...(opts?.timeout ? { timeout: opts.timeout } : {}) },
1204
+ undefined,
1205
+ (response) => "value" in response ? response.value : undefined,
1206
+ opts?.timeout,
1207
+ opts?.signal,
1208
+ ),
1209
+ editor: (title, prefill, opts) => this.requestExtensionUi(
1210
+ { method: "editor", title, ...(prefill !== undefined ? { prefill } : {}), ...(opts?.timeout ? { timeout: opts.timeout } : {}) },
1211
+ undefined,
1212
+ (response) => "value" in response ? response.value : undefined,
1213
+ opts?.timeout,
1214
+ opts?.signal,
1215
+ ),
1216
+ notify: (message, type) => {
1217
+ this.emit({
1218
+ type: "extension_ui_request",
1219
+ id: randomUUID(),
1220
+ method: "notify",
1221
+ message,
1222
+ notifyType: type,
1223
+ } as ExtensionUiRequest as AgentEvent);
1224
+ },
1225
+ onTerminalInput: () => () => {},
1226
+ setStatus: (key, text) => {
1227
+ if (text === undefined) this.extensionStatuses.delete(key);
1228
+ else this.extensionStatuses.set(key, text);
1229
+ this.emit({
1230
+ type: "extension_ui_request",
1231
+ id: randomUUID(),
1232
+ method: "setStatus",
1233
+ statusKey: key,
1234
+ statusText: text,
1235
+ } as ExtensionUiRequest as AgentEvent);
1236
+ },
1237
+ setWorkingMessage: () => {},
1238
+ setWorkingVisible: () => {},
1239
+ setWorkingIndicator: () => {},
1240
+ setHiddenThinkingLabel: () => {},
1241
+ setWidget: (key, content, options) => {
1242
+ if (!this._alive || this.extensionWidgetsResetting) return;
1243
+ if (typeof content === "function") {
1244
+ this.setExtensionWidgetFactory(
1245
+ key,
1246
+ content as unknown as ExtensionWidgetFactory,
1247
+ options,
1248
+ );
1249
+ return;
1250
+ }
1251
+ if (content !== undefined && !Array.isArray(content)) return;
1252
+ if (content === undefined) {
1253
+ this.clearExtensionWidget(key);
1254
+ return;
1255
+ }
1256
+ const generation = this.activeExtensionWidgets.has(key)
1257
+ ? this.clearExtensionWidget(key)
1258
+ : this.nextExtensionWidgetGeneration(key);
1259
+ if (this.extensionWidgetGenerations.get(key) !== generation) return;
1260
+ this.extensionWidgets.set(key, {
1261
+ key,
1262
+ lines: content,
1263
+ placement: options?.placement ?? "aboveEditor",
1264
+ });
1265
+ this.emit({
1266
+ type: "extension_ui_request",
1267
+ id: randomUUID(),
1268
+ method: "setWidget",
1269
+ widgetKey: key,
1270
+ widgetLines: content,
1271
+ widgetPlacement: options?.placement,
1272
+ } as ExtensionUiRequest as AgentEvent);
1273
+ },
1274
+ setFooter: () => {},
1275
+ setHeader: () => {},
1276
+ setTitle: (title) => {
1277
+ this.emit({
1278
+ type: "extension_ui_request",
1279
+ id: randomUUID(),
1280
+ method: "setTitle",
1281
+ title,
1282
+ } as ExtensionUiRequest as AgentEvent);
1283
+ },
1284
+ custom: <T = unknown>(factory: unknown, options?: unknown) => this.requestExtensionCustomUi<T>(factory, options),
1285
+ pasteToEditor: (text) => {
1286
+ this.emit({
1287
+ type: "extension_ui_request",
1288
+ id: randomUUID(),
1289
+ method: "set_editor_text",
1290
+ text,
1291
+ } as ExtensionUiRequest as AgentEvent);
1292
+ },
1293
+ setEditorText: (text) => {
1294
+ this.emit({
1295
+ type: "extension_ui_request",
1296
+ id: randomUUID(),
1297
+ method: "set_editor_text",
1298
+ text,
1299
+ } as ExtensionUiRequest as AgentEvent);
1300
+ },
1301
+ getEditorText: () => "",
1302
+ addAutocompleteProvider: () => {},
1303
+ setEditorComponent: () => {},
1304
+ getEditorComponent: () => undefined,
1305
+ get theme() { return PLAIN_TEXT_THEME; },
1306
+ getAllThemes: () => [],
1307
+ getTheme: () => undefined,
1308
+ setTheme: () => ({ success: false, error: "Theme switching is not supported in Pi Web extension UI yet" }),
1309
+ getToolsExpanded: () => false,
1310
+ setToolsExpanded: () => {},
1311
+ };
1312
+ }
1313
+
1314
+ private createExtensionCommandContextActions(): ExtensionCommandContextActionsLike {
1315
+ return {
1316
+ waitForIdle: async () => {
1317
+ const agent = this.inner.agent as { waitForIdle?: () => Promise<void> };
1318
+ await agent.waitForIdle?.();
1319
+ },
1320
+ newSession: async () => ({ cancelled: true }),
1321
+ fork: async () => ({ cancelled: true }),
1322
+ navigateTree: async (targetId, options) => {
1323
+ const result = await this.inner.navigateTree(targetId, { summarize: options?.summarize });
1324
+ return { cancelled: result.cancelled };
1325
+ },
1326
+ switchSession: async () => ({ cancelled: true }),
1327
+ reload: async () => {
1328
+ this.extensionStatuses.clear();
1329
+ this.resetExtensionWidgetsForReload();
1330
+ this.syncProjectTrust();
1331
+ await this.inner.reload({
1332
+ beforeSessionStart: () => {
1333
+ this.inner.extensionRunner.setUIContext?.(this.createExtensionUiContext(), "rpc");
1334
+ },
1335
+ });
1336
+ this.applyForcedEmptySystemPrompt();
1337
+ },
1338
+ };
1339
+ }
1340
+
1341
+ private syncProjectTrust(): void {
1342
+ const status = getProjectTrustStatus(this.cwd, getAgentDir());
1343
+ this.inner.settingsManager.setProjectTrusted(status.trusted);
1344
+ }
1345
+ }
1346
+
1347
+ // ============================================================================
1348
+ // Session registry
1349
+ // ============================================================================
1350
+
1351
+ declare global {
1352
+ var __piSessions: Map<string, AgentSessionWrapper> | undefined;
1353
+ var __piStartLocks: Map<string, Promise<{ session: AgentSessionWrapper; realSessionId: string }>> | undefined;
1354
+ var __piStartingSessionCwds: Map<string, number> | undefined;
1355
+ }
1356
+
1357
+ function getRegistry(): Map<string, AgentSessionWrapper> {
1358
+ if (!globalThis.__piSessions) {
1359
+ globalThis.__piSessions = new Map();
1360
+ const cleanup = () => globalThis.__piSessions?.forEach((s) => s.destroy());
1361
+ process.once("exit", cleanup);
1362
+ process.once("SIGINT", cleanup);
1363
+ process.once("SIGTERM", cleanup);
1364
+ }
1365
+ return globalThis.__piSessions;
1366
+ }
1367
+
1368
+ function getLocks(): Map<string, Promise<{ session: AgentSessionWrapper; realSessionId: string }>> {
1369
+ if (!globalThis.__piStartLocks) globalThis.__piStartLocks = new Map();
1370
+ return globalThis.__piStartLocks;
1371
+ }
1372
+
1373
+ function normalizeRpcCwd(cwd: string): string {
1374
+ const resolvedCwd = resolve(cwd);
1375
+ try {
1376
+ return realpathSync(resolvedCwd);
1377
+ } catch {
1378
+ return resolvedCwd;
1379
+ }
1380
+ }
1381
+
1382
+ function getStartingSessionCwds(): Map<string, number> {
1383
+ if (!globalThis.__piStartingSessionCwds) globalThis.__piStartingSessionCwds = new Map();
1384
+ return globalThis.__piStartingSessionCwds;
1385
+ }
1386
+
1387
+ function trackStartingSession(cwd: string): () => void {
1388
+ const startingCwds = getStartingSessionCwds();
1389
+ const key = normalizeRpcCwd(cwd);
1390
+ startingCwds.set(key, (startingCwds.get(key) ?? 0) + 1);
1391
+ return () => {
1392
+ const remaining = (startingCwds.get(key) ?? 1) - 1;
1393
+ if (remaining > 0) startingCwds.set(key, remaining);
1394
+ else startingCwds.delete(key);
1395
+ };
1396
+ }
1397
+
1398
+ /**
1399
+ * 新建会话创建即落盘:写空 header 文件并标记 flushed。
1400
+ * pi 默认延迟到第一条 assistant 消息才 flush(避免生成空会话文件),但
1401
+ * pi-web 的会话 ID 由创建方指定(任务/看板绑定在创建时就写库),若文件不
1402
+ * 落盘,刷新后 /api/sessions 读不到该会话,绑定就会“丢失”。
1403
+ * 调用后 manager 的后续 append 走 appendFileSync(见 SDK _persist)。
1404
+ */
1405
+ function persistNewSessionFile(manager: SessionManager, sessionId: string): void {
1406
+ const sessionFile = manager.getSessionFile();
1407
+ if (!sessionFile || existsSync(sessionFile)) return;
1408
+ const header = manager.getHeader();
1409
+ if (!header) return;
1410
+ const content = [header, ...manager.getEntries()]
1411
+ .map((entry) => JSON.stringify(entry))
1412
+ .join("\n") + "\n";
1413
+ writeFileSync(sessionFile, content, { encoding: "utf8", flag: "wx" });
1414
+ (manager as unknown as { flushed: boolean }).flushed = true;
1415
+ cacheSessionPath(sessionId, sessionFile);
1416
+ }
1417
+
1418
+ export function getRpcSession(sessionId: string): AgentSessionWrapper | undefined {
1419
+ return getRegistry().get(sessionId);
1420
+ }
1421
+
1422
+ function runtimeMessageText(entry: SessionMessageEntry): string {
1423
+ if (entry.message.role === "bashExecution") return "";
1424
+ const content = entry.message.content;
1425
+ if (typeof content === "string") return content;
1426
+ return content
1427
+ .map((block) => block.type === "text" ? block.text : "")
1428
+ .filter(Boolean)
1429
+ .join(" ");
1430
+ }
1431
+
1432
+ function runtimeMessageActivityMs(entry: SessionMessageEntry): number | undefined {
1433
+ if (entry.message.role !== "user" && entry.message.role !== "assistant") return undefined;
1434
+ if (typeof entry.message.timestamp === "number") return entry.message.timestamp;
1435
+ const timestamp = new Date(entry.timestamp).getTime();
1436
+ return Number.isNaN(timestamp) ? undefined : timestamp;
1437
+ }
1438
+
1439
+ /**
1440
+ * Return live sessions that should be visible in the session list. Pi delays
1441
+ * the first JSONL flush until an assistant message exists, so an accepted new
1442
+ * prompt must temporarily be described from its in-memory SessionManager.
1443
+ */
1444
+ export function getRpcSessionInfos(): SessionInfo[] {
1445
+ const sessions: SessionInfo[] = [];
1446
+ for (const session of getRegistry().values()) {
1447
+ if (!session.isAlive()) continue;
1448
+
1449
+ const manager = session.inner.sessionManager;
1450
+ const header = manager.getHeader();
1451
+ const entries = manager.getEntries() as unknown as Array<
1452
+ { type: string; timestamp: string } | SessionMessageEntry
1453
+ >;
1454
+ const messages = entries.filter((entry): entry is SessionMessageEntry => entry.type === "message");
1455
+ const firstUserMessage = messages.find((entry) => entry.message.role === "user");
1456
+ const sessionFile = manager.getSessionFile() ?? session.sessionFile;
1457
+ const persisted = Boolean(sessionFile && existsSync(sessionFile));
1458
+
1459
+ // An ensure_session call creates an idle, empty runtime while the composer
1460
+ // loads commands. Do not leak it into history before a prompt is accepted.
1461
+ if (!persisted && (!session.isRunning() || !firstUserMessage)) continue;
1462
+
1463
+ const created = header?.timestamp
1464
+ ?? entries[0]?.timestamp
1465
+ ?? new Date().toISOString();
1466
+ const headerTimestamp = new Date(created).getTime();
1467
+ let lastActivityMs = Number.isNaN(headerTimestamp) ? Date.now() : headerTimestamp;
1468
+ for (const message of messages) {
1469
+ const activityMs = runtimeMessageActivityMs(message);
1470
+ if (activityMs !== undefined) lastActivityMs = Math.max(lastActivityMs, activityMs);
1471
+ }
1472
+
1473
+ sessions.push({
1474
+ path: sessionFile ?? "",
1475
+ id: header?.id ?? session.sessionId,
1476
+ cwd: header?.cwd ?? session.cwd,
1477
+ name: manager.getSessionName(),
1478
+ created,
1479
+ modified: new Date(lastActivityMs).toISOString(),
1480
+ messageCount: messages.length,
1481
+ firstMessage: firstUserMessage ? runtimeMessageText(firstUserMessage) || "(no messages)" : "(no messages)",
1482
+ transient: !persisted,
1483
+ });
1484
+ }
1485
+ return sessions;
1486
+ }
1487
+
1488
+ export function hasBusyRpcSessionForCwd(cwd: string): boolean {
1489
+ const targetCwd = normalizeRpcCwd(cwd);
1490
+ if (getStartingSessionCwds().has(targetCwd)) return true;
1491
+ return Array.from(getRegistry().values()).some(
1492
+ (session) => normalizeRpcCwd(session.cwd) === targetCwd && session.isRunning(),
1493
+ );
1494
+ }
1495
+
1496
+ export async function destroyRpcSessionsForCwd(cwd: string): Promise<number> {
1497
+ const targetCwd = normalizeRpcCwd(cwd);
1498
+ const sessions = Array.from(getRegistry().values()).filter(
1499
+ (session) => normalizeRpcCwd(session.cwd) === targetCwd,
1500
+ );
1501
+ await Promise.all(sessions.map((session) => session.shutdown()));
1502
+ return sessions.length;
1503
+ }
1504
+
1505
+ export function getRunningRpcSessionIds(): string[] {
1506
+ const ids = new Set<string>();
1507
+ for (const [sessionId, session] of getRegistry()) {
1508
+ if (session.isRunning()) ids.add(session.sessionId || sessionId);
1509
+ }
1510
+ return [...ids];
1511
+ }
1512
+
1513
+ /**
1514
+ * 运行中会话的细分状态快照(看板状态系统用)。
1515
+ * 状态判定优先级(与 pi 既有语义对齐):
1516
+ * 1. 等待模型:isStreaming 且无待执行工具调用(thinking / 生成中)
1517
+ * 2. 执行工具:pendingToolCalls 非空(running_tools)
1518
+ * 3. 执行命令:isBashRunning(running_command)
1519
+ * 4. 等待输入:扩展 UI 请求(select/confirm/input/editor)挂起(waiting_input)
1520
+ * startedAt 用最近一次 agent_start 事件时间(wrapper 内订阅缓存),未知为 0。
1521
+ */
1522
+ export function getRunningSessionStates(): Record<string, RunningSessionState> {
1523
+ const states: Record<string, RunningSessionState> = {};
1524
+ for (const [sessionId, session] of getRegistry()) {
1525
+ const id = session.sessionId || sessionId;
1526
+ if (!session.isRunning()) continue;
1527
+ const inner = session.inner;
1528
+ const state = inner.agent.state as { pendingToolCalls?: ReadonlySet<string> } | undefined;
1529
+ const pendingToolCalls = state?.pendingToolCalls;
1530
+ let phase: RunningPhase;
1531
+ if (inner.isStreaming && pendingToolCalls && pendingToolCalls.size > 0) {
1532
+ phase = "running_tools";
1533
+ } else if (inner.isBashRunning) {
1534
+ phase = "running_command";
1535
+ } else if (inner.isStreaming) {
1536
+ phase = "waiting_model";
1537
+ } else if (session.hasPendingUiRequest()) {
1538
+ phase = "waiting_input";
1539
+ } else {
1540
+ phase = "waiting_model";
1541
+ }
1542
+ states[id] = {
1543
+ phase,
1544
+ model: inner.model ? `${inner.model.provider}/${inner.model.id}` : null,
1545
+ startedAt: session.lastAgentStartAt(),
1546
+ };
1547
+ }
1548
+ return states;
1549
+ }
1550
+
1551
+ /**
1552
+ * Get or create an AgentSession for the given session.
1553
+ * For new sessions (sessionFile === ""), pi generates its own id.
1554
+ * New sessions resolve enabledModels before construction so the initial model,
1555
+ * thinking pin, and SDK scopedModels share one settings snapshot.
1556
+ * Pass options.toolNames to pre-configure active tools (empty = all disabled).
1557
+ */
1558
+ export async function startRpcSession(
1559
+ sessionId: string,
1560
+ sessionFile: string,
1561
+ cwd: string | undefined,
1562
+ options: RpcSessionStartOptions = {},
1563
+ ): Promise<{ session: AgentSessionWrapper; realSessionId: string }> {
1564
+ const { toolNames, initialModel, thinkingLevel } = options;
1565
+ const registry = getRegistry();
1566
+ const locks = getLocks();
1567
+
1568
+ const existing = registry.get(sessionId);
1569
+ if (existing?.isAlive()) return { session: existing, realSessionId: sessionId };
1570
+
1571
+ const inflight = locks.get(sessionId);
1572
+ if (inflight) return inflight;
1573
+
1574
+ let sessionManager: SessionManager;
1575
+ if (sessionFile) {
1576
+ sessionManager = SessionManager.open(sessionFile, undefined);
1577
+ } else {
1578
+ if (!cwd) throw new Error("cwd is required for a new session");
1579
+ // 指定会话 ID:合法的 sessionId 参数即最终会话 ID(SDK 原生支持 NewSessionOptions.id)。
1580
+ // 新建会话的 ID 在发起时即可知,任务/看板绑定不再依赖等待 realSessionId 返回。
1581
+ // 内部占位 key(如 route.ts 的 `__new__<uuid>`,双下划线前缀)不算指定 ID——
1582
+ // 它们只作并发锁 key,真实 ID 仍由 pi 生成(向后兼容无 id 的调用方)。
1583
+ const isExplicitId = sessionId.length > 0 && !sessionId.startsWith("__");
1584
+ sessionManager = SessionManager.create(cwd, undefined, isExplicitId ? { id: sessionId } : undefined);
1585
+ // 创建即落盘:会话文件从出生就在磁盘,刷新后绑定不丢(见 persistNewSessionFile)。
1586
+ if (isExplicitId) persistNewSessionFile(sessionManager, sessionId);
1587
+ }
1588
+ const sessionCwd = sessionManager.getCwd();
1589
+ const finishStartingSession = trackStartingSession(sessionCwd);
1590
+ const starting = (async () => {
1591
+ // Some extensions access the SDK's global theme even outside the terminal UI.
1592
+ initTheme();
1593
+ const agentDir = getAgentDir();
1594
+
1595
+ // Determine which tools to pass based on requested toolNames.
1596
+ // Since v0.68.0, session creation expects string[] tool names instead of Tool[] instances.
1597
+ let toolsOption: string[] | undefined;
1598
+ if (toolNames !== undefined) {
1599
+ // toolNames === [] -> "all off" (an empty allow-list disables every tool).
1600
+ // Otherwise DO NOT pass a builtin-only allow-list: passing CODING_TOOL_NAMES
1601
+ // set allowedToolNames to coding builtins only, which filtered every
1602
+ // extension/package-provided tool (e.g. subagents, web access) out of the
1603
+ // tool registry — so they were unavailable in Pi Web sessions even though the
1604
+ // `pi` CLI keeps them. Leaving the allow-list unset lets the SDK register all
1605
+ // tools (and activate extension tools); we narrow the ACTIVE set below.
1606
+ toolsOption = toolNames.length === 0 ? [] : undefined;
1607
+ }
1608
+
1609
+ // Build services first so extension-registered providers are available
1610
+ // before the SDK restores the saved model from the session file.
1611
+ // Gate untrusted project extensions so opening a repository does not run
1612
+ // its .pi/extensions code automatically (see lib/project-trust.ts, #236).
1613
+ const trustReloadOptions = projectTrustReloadOptions(sessionCwd, agentDir);
1614
+ const settingsManager = SettingsManager.create(sessionCwd, agentDir);
1615
+ const services = await createAgentSessionServices({
1616
+ cwd: sessionCwd,
1617
+ agentDir,
1618
+ settingsManager,
1619
+ resourceLoaderOptions: {
1620
+ extensionFactories: [
1621
+ createProjectCommandBashExtension({
1622
+ cwd: sessionCwd,
1623
+ settings: settingsManager,
1624
+ }),
1625
+ ],
1626
+ extensionsOverride: preferUserBashExtension,
1627
+ },
1628
+ ...(trustReloadOptions ? { resourceLoaderReloadOptions: trustReloadOptions } : {}),
1629
+ });
1630
+ const scope = await resolveVisibleModels(
1631
+ services.modelRuntime,
1632
+ services.settingsManager.getEnabledModels(),
1633
+ );
1634
+ const defaultProvider = services.settingsManager.getDefaultProvider();
1635
+ const defaultModelId = services.settingsManager.getDefaultModel();
1636
+ const hasExistingMessages = sessionManager.getBranch().some((entry) => entry.type === "message");
1637
+ const initial = hasExistingMessages
1638
+ ? { scopedModels: [...scope.scopedModels] }
1639
+ : selectInitialModelScope(scope, {
1640
+ ...(initialModel ? { requestedModel: initialModel } : {}),
1641
+ ...(defaultProvider && defaultModelId
1642
+ ? { defaultModel: { provider: defaultProvider, modelId: defaultModelId } }
1643
+ : {}),
1644
+ ...(thinkingLevel ? { thinkingLevel } : {}),
1645
+ });
1646
+ const { session: inner } = await createAgentSessionFromServices({
1647
+ services,
1648
+ sessionManager,
1649
+ ...(initial.model ? { model: initial.model } : {}),
1650
+ ...(initial.thinkingLevel ? { thinkingLevel: initial.thinkingLevel } : {}),
1651
+ ...(initial.scopedModels.length > 0 ? { scopedModels: initial.scopedModels } : {}),
1652
+ ...(toolsOption !== undefined ? { tools: toolsOption } : {}),
1653
+ });
1654
+
1655
+ const persistedPreferences = await persistExplicitStartupPreferences(
1656
+ services.settingsManager,
1657
+ {
1658
+ ...(initialModel ? { model: initialModel } : {}),
1659
+ ...(thinkingLevel ? { thinkingLevel } : {}),
1660
+ },
1661
+ {
1662
+ ...(inner.model
1663
+ ? { model: { provider: inner.model.provider, modelId: inner.model.id } }
1664
+ : {}),
1665
+ thinkingLevel: inner.thinkingLevel,
1666
+ supportsThinking: inner.supportsThinking(),
1667
+ },
1668
+ );
1669
+ if (persistedPreferences.modelDefaultChanged) invalidateModelsCache();
1670
+
1671
+ // If specific tool names were requested (non-empty), set the active tools to the
1672
+ // requested builtin coding tools PLUS all extension/package tools, so installed
1673
+ // extensions stay usable in Pi Web just like in the `pi` CLI.
1674
+ if (toolNames && toolNames.length > 0) {
1675
+ inner.setActiveToolsByName(withExtensionTools(inner, toolNames));
1676
+ }
1677
+
1678
+ const wrapper = new AgentSessionWrapper(inner);
1679
+ // When all tools are disabled, clear the system prompt entirely.
1680
+ // pi's buildSystemPrompt always produces a non-empty prompt even with no tools;
1681
+ // keep this forced after extension resource discovery and reloads as well.
1682
+ if (toolNames?.length === 0) {
1683
+ wrapper.setForceEmptySystemPrompt(true);
1684
+ }
1685
+ wrapper.start();
1686
+
1687
+ const realSessionId = inner.sessionId as string;
1688
+ const realSessionFile = inner.sessionFile as string | undefined;
1689
+ if (realSessionFile) cacheSessionPath(realSessionId, realSessionFile);
1690
+
1691
+ wrapper.onDestroy(() => registry.delete(realSessionId));
1692
+ registry.set(realSessionId, wrapper);
1693
+ wrapper.beginExtensionBinding({ forceEmptySystemPrompt: toolNames?.length === 0 });
1694
+
1695
+ return { session: wrapper, realSessionId };
1696
+ })().finally(() => {
1697
+ locks.delete(sessionId);
1698
+ finishStartingSession();
1699
+ });
1700
+
1701
+ locks.set(sessionId, starting);
1702
+ return starting;
1703
+ }