@rexvane/piweb 0.3.20 → 0.3.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (317) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +6 -4
  3. package/.next/build-manifest.json +5 -5
  4. package/.next/next-minimal-server.js.nft.json +1 -1
  5. package/.next/next-server.js.nft.json +1 -1
  6. package/.next/prerender-manifest.json +7 -7
  7. package/.next/react-loadable-manifest.json +12 -12
  8. package/.next/required-server-files.js +18 -2
  9. package/.next/required-server-files.json +18 -2
  10. package/.next/routes-manifest.json +12 -0
  11. package/.next/server/app/_global-error/page.js +2 -2
  12. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  13. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/_global-error.html +1 -1
  15. package/.next/server/app/_global-error.meta +8 -0
  16. package/.next/server/app/_global-error.rsc +15 -11
  17. package/.next/server/app/_global-error.segments/_full.segment.rsc +15 -11
  18. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +20 -16
  19. package/.next/server/app/_global-error.segments/_tree.segment.rsc +3 -1
  20. package/.next/server/app/_not-found/page.js +1 -1
  21. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  22. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  23. package/.next/server/app/_not-found.html +1 -1
  24. package/.next/server/app/_not-found.meta +8 -0
  25. package/.next/server/app/_not-found.rsc +17 -13
  26. package/.next/server/app/_not-found.segments/_full.segment.rsc +17 -13
  27. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +24 -20
  28. package/.next/server/app/_not-found.segments/_tree.segment.rsc +4 -2
  29. package/.next/server/app/api/agent/[id]/events/route.js +3 -3
  30. package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
  31. package/.next/server/app/api/agent/[id]/route.js +1 -1
  32. package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
  33. package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
  34. package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
  35. package/.next/server/app/api/agent/new/route.js +1 -1
  36. package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
  37. package/.next/server/app/api/config/route.js +1 -1
  38. package/.next/server/app/api/config/route.js.nft.json +1 -1
  39. package/.next/server/app/api/files/route.js +1 -1
  40. package/.next/server/app/api/files/route.js.nft.json +1 -1
  41. package/.next/server/app/api/fs/route.js +1 -1
  42. package/.next/server/app/api/fs/route.js.nft.json +1 -1
  43. package/.next/server/app/api/git/route.js +4 -4
  44. package/.next/server/app/api/git/route.js.nft.json +1 -1
  45. package/.next/server/app/api/github/route.js +5 -0
  46. package/.next/server/app/api/github/route.js.nft.json +1 -0
  47. package/.next/server/app/api/github/route_client-reference-manifest.js +1 -0
  48. package/.next/server/app/api/growth/route.js +1 -1
  49. package/.next/server/app/api/growth/route.js.nft.json +1 -1
  50. package/.next/server/app/api/health/route.js +1 -1
  51. package/.next/server/app/api/health/route.js.nft.json +1 -1
  52. package/.next/server/app/api/models/defaults/route.js +1 -0
  53. package/.next/server/app/api/models/defaults/route.js.nft.json +1 -0
  54. package/.next/server/app/api/models/defaults/route_client-reference-manifest.js +1 -0
  55. package/.next/server/app/api/models/route.js +1 -1
  56. package/.next/server/app/api/models/route.js.nft.json +1 -1
  57. package/.next/server/app/api/pi-settings/route.js +1 -1
  58. package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
  59. package/.next/server/app/api/plugins/route.js +1 -1
  60. package/.next/server/app/api/plugins/route.js.nft.json +1 -1
  61. package/.next/server/app/api/prompts/route.js +1 -1
  62. package/.next/server/app/api/prompts/route.js.nft.json +1 -1
  63. package/.next/server/app/api/remote/route.js +3 -3
  64. package/.next/server/app/api/remote/route.js.nft.json +1 -1
  65. package/.next/server/app/api/security/route.js +1 -1
  66. package/.next/server/app/api/security/route.js.nft.json +1 -1
  67. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  68. package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
  69. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  70. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
  71. package/.next/server/app/api/sessions/import/route.js +2 -2
  72. package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
  73. package/.next/server/app/api/sessions/route.js +1 -1
  74. package/.next/server/app/api/sessions/route.js.nft.json +1 -1
  75. package/.next/server/app/api/skills/route.js +2 -2
  76. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  77. package/.next/server/app/api/update/route.js +2 -2
  78. package/.next/server/app/api/update/route.js.nft.json +1 -1
  79. package/.next/server/app/api/version/route.js +1 -1
  80. package/.next/server/app/api/version/route.js.nft.json +1 -1
  81. package/.next/server/app/api/web-auth/route.js +1 -1
  82. package/.next/server/app/api/web-auth/route.js.nft.json +1 -1
  83. package/.next/server/app/api/workspaces/route.js +1 -1
  84. package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
  85. package/.next/server/app/icon.svg/route.js +1 -1
  86. package/.next/server/app/icon.svg/route.js.nft.json +1 -1
  87. package/.next/server/app/icon.svg.meta +1 -1
  88. package/.next/server/app/index.html +1 -1
  89. package/.next/server/app/index.meta +8 -0
  90. package/.next/server/app/index.rsc +19 -13
  91. package/.next/server/app/index.segments/__PAGE__.segment.rsc +25 -19
  92. package/.next/server/app/index.segments/_full.segment.rsc +19 -13
  93. package/.next/server/app/index.segments/_tree.segment.rsc +3 -2
  94. package/.next/server/app/login/page.js +1 -1
  95. package/.next/server/app/login/page.js.nft.json +1 -1
  96. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  97. package/.next/server/app/login.html +1 -1
  98. package/.next/server/app/login.meta +8 -0
  99. package/.next/server/app/login.rsc +23 -17
  100. package/.next/server/app/login.segments/_full.segment.rsc +23 -17
  101. package/.next/server/app/login.segments/_tree.segment.rsc +4 -2
  102. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +30 -24
  103. package/.next/server/app/page.js +1 -1
  104. package/.next/server/app/page.js.nft.json +1 -1
  105. package/.next/server/app/page_client-reference-manifest.js +1 -1
  106. package/.next/server/app-paths-manifest.json +6 -4
  107. package/.next/server/chunks/229.js +1 -1
  108. package/.next/server/chunks/421.js +1 -0
  109. package/.next/server/chunks/430.js +215 -31
  110. package/.next/server/chunks/496.js +1 -1
  111. package/.next/server/chunks/538.js +1 -1
  112. package/.next/server/chunks/662.js +2 -0
  113. package/.next/server/chunks/686.js +1 -1
  114. package/.next/server/chunks/807.js +1 -1
  115. package/.next/server/chunks/813.js +23 -1
  116. package/.next/server/chunks/920.js +2 -2
  117. package/.next/server/chunks/924.js +1 -1
  118. package/.next/server/chunks/970.js +305 -0
  119. package/.next/server/instrumentation.js +1 -1
  120. package/.next/server/instrumentation.js.nft.json +1 -1
  121. package/.next/server/middleware-build-manifest.js +1 -1
  122. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  123. package/.next/server/middleware.js +185 -23
  124. package/.next/server/middleware.js.nft.json +1 -1
  125. package/.next/server/pages/404.html +1 -1
  126. package/.next/server/pages/500.html +1 -1
  127. package/.next/server/preview-props.json +5 -0
  128. package/.next/server/server-reference-manifest.json +1 -1
  129. package/.next/static/WMazYvD-gbeLsCUfUxvvM/_buildManifest.js +1 -0
  130. package/.next/static/chunks/316-698d10eb465bd8ef.js +1 -0
  131. package/.next/static/chunks/3794-0978865cda773008.js +52 -0
  132. package/.next/static/chunks/4673.4cf66cbdd8364c57.js +1 -0
  133. package/.next/static/chunks/4686-09fcd0028079006c.js +1 -0
  134. package/.next/static/chunks/4778.eddd3a5813f85824.js +1 -0
  135. package/.next/static/chunks/4bd1b696-e199e788e58acf79.js +1 -0
  136. package/.next/static/chunks/{5173.91ddb803785009f8.js → 5173.dfd1cbb795b9a21c.js} +1 -1
  137. package/.next/static/chunks/618.949981da99be7182.js +4 -0
  138. package/.next/static/chunks/7411.67366dc5032a141d.js +1 -0
  139. package/.next/static/chunks/8552.25c39ef4c995eba0.js +1 -0
  140. package/.next/static/chunks/app/_global-error/page-f977af7f858990a2.js +1 -0
  141. package/.next/static/chunks/app/api/agent/[id]/events/route-f977af7f858990a2.js +1 -0
  142. package/.next/static/chunks/app/api/agent/[id]/route-f977af7f858990a2.js +1 -0
  143. package/.next/static/chunks/app/api/agent/[id]/tree/route-f977af7f858990a2.js +1 -0
  144. package/.next/static/chunks/app/api/agent/new/route-f977af7f858990a2.js +1 -0
  145. package/.next/static/chunks/app/api/config/route-f977af7f858990a2.js +1 -0
  146. package/.next/static/chunks/app/api/files/route-f977af7f858990a2.js +1 -0
  147. package/.next/static/chunks/app/api/fs/route-f977af7f858990a2.js +1 -0
  148. package/.next/static/chunks/app/api/git/route-f977af7f858990a2.js +1 -0
  149. package/.next/static/chunks/app/api/github/route-f977af7f858990a2.js +1 -0
  150. package/.next/static/chunks/app/api/growth/route-f977af7f858990a2.js +1 -0
  151. package/.next/static/chunks/app/api/health/route-f977af7f858990a2.js +1 -0
  152. package/.next/static/chunks/app/api/models/defaults/route-f977af7f858990a2.js +1 -0
  153. package/.next/static/chunks/app/api/models/route-f977af7f858990a2.js +1 -0
  154. package/.next/static/chunks/app/api/pi-settings/route-f977af7f858990a2.js +1 -0
  155. package/.next/static/chunks/app/api/plugins/route-f977af7f858990a2.js +1 -0
  156. package/.next/static/chunks/app/api/prompts/route-f977af7f858990a2.js +1 -0
  157. package/.next/static/chunks/app/api/remote/route-f977af7f858990a2.js +1 -0
  158. package/.next/static/chunks/app/api/security/route-f977af7f858990a2.js +1 -0
  159. package/.next/static/chunks/app/api/sessions/[id]/export/route-f977af7f858990a2.js +1 -0
  160. package/.next/static/chunks/app/api/sessions/[id]/route-f977af7f858990a2.js +1 -0
  161. package/.next/static/chunks/app/api/sessions/import/route-f977af7f858990a2.js +1 -0
  162. package/.next/static/chunks/app/api/sessions/route-f977af7f858990a2.js +1 -0
  163. package/.next/static/chunks/app/api/skills/route-f977af7f858990a2.js +1 -0
  164. package/.next/static/chunks/app/api/update/route-f977af7f858990a2.js +1 -0
  165. package/.next/static/chunks/app/api/version/route-f977af7f858990a2.js +1 -0
  166. package/.next/static/chunks/app/api/web-auth/route-f977af7f858990a2.js +1 -0
  167. package/.next/static/chunks/app/api/workspaces/route-f977af7f858990a2.js +1 -0
  168. package/.next/static/chunks/app/page-fc4ed2ff29b6235e.js +1 -0
  169. package/.next/static/chunks/framework-47b723d27f4a4963.js +1 -0
  170. package/.next/static/chunks/main-5403f0764579e5fe.js +5 -0
  171. package/.next/static/chunks/next/dist/client/components/builtin/app-error-f977af7f858990a2.js +1 -0
  172. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f977af7f858990a2.js +1 -0
  173. package/.next/static/chunks/next/dist/client/components/builtin/not-found-f977af7f858990a2.js +1 -0
  174. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f977af7f858990a2.js +1 -0
  175. package/.next/static/chunks/webpack-22ebffc20adb44ae.js +1 -0
  176. package/.next/static/css/6671d0d1589fdc0e.css +1 -0
  177. package/.next/static/css/{2959bd6b626b7ffe.css → f15a853af8b5127f.css} +1 -1
  178. package/.next/types/app/api/agent/[id]/events/route.ts +3 -1
  179. package/.next/types/app/api/agent/[id]/route.ts +3 -1
  180. package/.next/types/app/api/agent/[id]/tree/route.ts +3 -1
  181. package/.next/types/app/api/agent/new/route.ts +3 -1
  182. package/.next/types/app/api/config/route.ts +3 -1
  183. package/.next/types/app/api/files/route.ts +3 -1
  184. package/.next/types/app/api/fs/route.ts +3 -1
  185. package/.next/types/app/api/git/route.ts +3 -1
  186. package/.next/types/app/api/github/route.ts +355 -0
  187. package/.next/types/app/api/growth/route.ts +3 -1
  188. package/.next/types/app/api/health/route.ts +3 -1
  189. package/.next/types/app/api/models/defaults/route.ts +355 -0
  190. package/.next/types/app/api/models/route.ts +3 -1
  191. package/.next/types/app/api/pi-settings/route.ts +3 -1
  192. package/.next/types/app/api/plugins/route.ts +3 -1
  193. package/.next/types/app/api/prompts/route.ts +3 -1
  194. package/.next/types/app/api/remote/route.ts +3 -1
  195. package/.next/types/app/api/security/route.ts +3 -1
  196. package/.next/types/app/api/sessions/[id]/export/route.ts +3 -1
  197. package/.next/types/app/api/sessions/[id]/route.ts +3 -1
  198. package/.next/types/app/api/sessions/import/route.ts +3 -1
  199. package/.next/types/app/api/sessions/route.ts +3 -1
  200. package/.next/types/app/api/skills/route.ts +3 -1
  201. package/.next/types/app/api/update/route.ts +3 -1
  202. package/.next/types/app/api/version/route.ts +3 -1
  203. package/.next/types/app/api/web-auth/route.ts +3 -1
  204. package/.next/types/app/api/workspaces/route.ts +3 -1
  205. package/.next/types/app/layout.ts +4 -1
  206. package/.next/types/app/login/page.ts +4 -1
  207. package/.next/types/app/page.ts +4 -1
  208. package/.next/types/cache-life.d.ts +11 -43
  209. package/.next/types/routes.d.ts +3 -1
  210. package/.next/types/validator.ts +76 -2
  211. package/README.md +14 -5
  212. package/next.config.ts +4 -2
  213. package/package.json +2 -2
  214. package/scripts/release.mjs +1 -1
  215. package/src/app/api/agent/[id]/events/route.ts +42 -13
  216. package/src/app/api/files/route.ts +5 -2
  217. package/src/app/api/github/route.ts +26 -0
  218. package/src/app/api/models/defaults/route.ts +18 -0
  219. package/src/app/api/models/route.ts +24 -1
  220. package/src/app/api/update/route.ts +18 -2
  221. package/src/app/globals.css +275 -0
  222. package/src/components/AppShell.tsx +89 -33
  223. package/src/components/ChatInput.tsx +245 -63
  224. package/src/components/ChatWindow.tsx +123 -108
  225. package/src/components/ContextMeter.tsx +204 -123
  226. package/src/components/DrawerPanel.tsx +19 -26
  227. package/src/components/ExtensionUI.tsx +8 -2
  228. package/src/components/GitPanel.tsx +110 -40
  229. package/src/components/MarkdownBlock.tsx +333 -37
  230. package/src/components/ProjectPanel.tsx +55 -30
  231. package/src/components/PromptPanel.tsx +2 -4
  232. package/src/components/ProviderSetupModal.module.css +20 -0
  233. package/src/components/ProviderSetupModal.tsx +134 -29
  234. package/src/components/SettingsPanel.tsx +365 -80
  235. package/src/components/TrajectoryView.tsx +4 -6
  236. package/src/components/chat/chat-scroll.ts +18 -0
  237. package/src/components/composer/ComposerImages.tsx +306 -0
  238. package/src/components/update/CopyCommand.tsx +34 -0
  239. package/src/components/update/UpdateNotice.tsx +130 -0
  240. package/src/components/update/update-store.ts +155 -0
  241. package/src/hooks/useGrowth.ts +3 -0
  242. package/src/hooks/usePiWeb.ts +54 -27
  243. package/src/hooks/useWorkspaceFiles.ts +93 -0
  244. package/src/i18n.tsx +182 -6
  245. package/src/instrumentation-node.ts +3 -0
  246. package/src/lib/agent-manager.ts +193 -119
  247. package/src/lib/chat/event-stream.ts +113 -0
  248. package/src/lib/chat/stream-settings.ts +192 -0
  249. package/src/lib/files-service.ts +100 -1
  250. package/src/lib/format.ts +56 -0
  251. package/src/lib/git-service.ts +45 -13
  252. package/src/lib/github-identity.ts +245 -0
  253. package/src/lib/growth-service.ts +6 -2
  254. package/src/lib/hero-model-memory.ts +28 -0
  255. package/src/lib/model-defaults.ts +105 -0
  256. package/src/lib/model-draft.ts +257 -9
  257. package/src/lib/model-endpoint.ts +23 -0
  258. package/src/lib/model-files.ts +33 -0
  259. package/src/lib/models-service.ts +240 -78
  260. package/src/lib/pi.ts +27 -2
  261. package/src/lib/process-format.ts +3 -2
  262. package/src/lib/session/metrics.ts +31 -0
  263. package/src/lib/thinking-level.ts +39 -0
  264. package/src/lib/trajectory.ts +17 -3
  265. package/src/lib/types.ts +19 -1
  266. package/src/lib/update/startup-check.ts +117 -0
  267. package/src/lib/update-service.ts +23 -3
  268. package/src/lib/workspace-index.ts +270 -0
  269. package/.next/server/chunks/131.js +0 -1
  270. package/.next/server/chunks/243.js +0 -1
  271. package/.next/server/chunks/741.js +0 -165
  272. package/.next/static/_Y3Lq0lI5RJ1tFSOnDi0L/_buildManifest.js +0 -1
  273. package/.next/static/chunks/316-5ea443ee6b512aa9.js +0 -1
  274. package/.next/static/chunks/3794-3679f1c05160977c.js +0 -52
  275. package/.next/static/chunks/4673.8a5a9e6855350e34.js +0 -1
  276. package/.next/static/chunks/4686-b1e9fa876d0ea671.js +0 -1
  277. package/.next/static/chunks/4778.e4c9ed093a413bc4.js +0 -1
  278. package/.next/static/chunks/4bd1b696-93bf506b236d6248.js +0 -1
  279. package/.next/static/chunks/618.ee5d5bf1c647e483.js +0 -4
  280. package/.next/static/chunks/839.d3fe337a9117c325.js +0 -1
  281. package/.next/static/chunks/8552.6bfe3e101250b20f.js +0 -1
  282. package/.next/static/chunks/app/_global-error/page-b179d9f7f6f2daf9.js +0 -1
  283. package/.next/static/chunks/app/api/agent/[id]/events/route-b179d9f7f6f2daf9.js +0 -1
  284. package/.next/static/chunks/app/api/agent/[id]/route-b179d9f7f6f2daf9.js +0 -1
  285. package/.next/static/chunks/app/api/agent/[id]/tree/route-b179d9f7f6f2daf9.js +0 -1
  286. package/.next/static/chunks/app/api/agent/new/route-b179d9f7f6f2daf9.js +0 -1
  287. package/.next/static/chunks/app/api/config/route-b179d9f7f6f2daf9.js +0 -1
  288. package/.next/static/chunks/app/api/files/route-b179d9f7f6f2daf9.js +0 -1
  289. package/.next/static/chunks/app/api/fs/route-b179d9f7f6f2daf9.js +0 -1
  290. package/.next/static/chunks/app/api/git/route-b179d9f7f6f2daf9.js +0 -1
  291. package/.next/static/chunks/app/api/growth/route-b179d9f7f6f2daf9.js +0 -1
  292. package/.next/static/chunks/app/api/health/route-b179d9f7f6f2daf9.js +0 -1
  293. package/.next/static/chunks/app/api/models/route-b179d9f7f6f2daf9.js +0 -1
  294. package/.next/static/chunks/app/api/pi-settings/route-b179d9f7f6f2daf9.js +0 -1
  295. package/.next/static/chunks/app/api/plugins/route-b179d9f7f6f2daf9.js +0 -1
  296. package/.next/static/chunks/app/api/prompts/route-b179d9f7f6f2daf9.js +0 -1
  297. package/.next/static/chunks/app/api/remote/route-b179d9f7f6f2daf9.js +0 -1
  298. package/.next/static/chunks/app/api/security/route-b179d9f7f6f2daf9.js +0 -1
  299. package/.next/static/chunks/app/api/sessions/[id]/export/route-b179d9f7f6f2daf9.js +0 -1
  300. package/.next/static/chunks/app/api/sessions/[id]/route-b179d9f7f6f2daf9.js +0 -1
  301. package/.next/static/chunks/app/api/sessions/import/route-b179d9f7f6f2daf9.js +0 -1
  302. package/.next/static/chunks/app/api/sessions/route-b179d9f7f6f2daf9.js +0 -1
  303. package/.next/static/chunks/app/api/skills/route-b179d9f7f6f2daf9.js +0 -1
  304. package/.next/static/chunks/app/api/update/route-b179d9f7f6f2daf9.js +0 -1
  305. package/.next/static/chunks/app/api/version/route-b179d9f7f6f2daf9.js +0 -1
  306. package/.next/static/chunks/app/api/web-auth/route-b179d9f7f6f2daf9.js +0 -1
  307. package/.next/static/chunks/app/api/workspaces/route-b179d9f7f6f2daf9.js +0 -1
  308. package/.next/static/chunks/app/page-2e425ef83fba0782.js +0 -1
  309. package/.next/static/chunks/framework-c4e651fe89edfa91.js +0 -1
  310. package/.next/static/chunks/main-6159b22a8cb0b7c5.js +0 -5
  311. package/.next/static/chunks/next/dist/client/components/builtin/app-error-b179d9f7f6f2daf9.js +0 -1
  312. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-b179d9f7f6f2daf9.js +0 -1
  313. package/.next/static/chunks/next/dist/client/components/builtin/not-found-b179d9f7f6f2daf9.js +0 -1
  314. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-b179d9f7f6f2daf9.js +0 -1
  315. package/.next/static/chunks/webpack-3614a8337843456b.js +0 -1
  316. package/.next/static/css/7391c42e1ae617f9.css +0 -1
  317. /package/.next/static/{_Y3Lq0lI5RJ1tFSOnDi0L → WMazYvD-gbeLsCUfUxvvM}/_ssgManifest.js +0 -0
@@ -6,7 +6,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
6
6
  import { randomUUID } from "node:crypto";
7
7
  import path from "node:path";
8
8
  import type { AgentSession, AgentSessionEvent } from "@earendil-works/pi-coding-agent";
9
- import { generateUnifiedPatch } from "@earendil-works/pi-coding-agent";
9
+ import { formatSkillsForPrompt, generateUnifiedPatch } from "@earendil-works/pi-coding-agent";
10
10
  import {
11
11
  getResourceLoader,
12
12
  createSessionResourceLoader,
@@ -25,17 +25,18 @@ import {
25
25
  SessionManager,
26
26
  createAgentSession,
27
27
  getModelRuntime,
28
+ refreshModelRuntime,
28
29
  openSessionManager,
29
30
  } from "./pi";
30
31
  import { createExtensionUiBridge, type ExtensionUiBridge } from "./extension-ui";
31
32
  import { createGrowthTracker, GROWTH_TRACKER_VERSION, type GrowthTracker } from "./growth-tracker";
32
33
  import { userTurnsFromEntries } from "./growth-turns";
34
+ import { projectedContextUsage } from "./session/metrics";
33
35
  import { TrajLedger, buildTrajectoryFromEntries, toTrajTokens } from "./trajectory";
34
36
  import { startArchivedSessionRetention } from "./session-retention";
35
37
  import { sanitizeToolOutput } from "./text-sanitize";
36
38
  import { BoundaryError } from "./path-security";
37
- import { estimateTokensOf } from "./process-format";
38
- import { getPiSettings } from "./pi-settings";
39
+ import { newSessionThinkingLevel } from "./thinking-level";
39
40
  import { getGrowthInitConsent, setGrowthInitConsent } from "./workspace-store";
40
41
  import { clampThinkingLevel, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
41
42
  import type {
@@ -63,6 +64,12 @@ interface Managed {
63
64
  session: AgentSession | null;
64
65
  creating: Promise<AgentSession> | null;
65
66
  subscribers: Set<(seq: number, json: string) => void>;
67
+ /** 长轮询的浏览器在两次请求之间没有订阅,到这个时间点之前仍算有人在看(见 holdViewer) */
68
+ viewerGraceUntil?: number;
69
+ /** 本轮已排队推送上下文分项(publishContextBreakdown 合并同一轮的多次触发) */
70
+ breakdownQueued?: boolean;
71
+ /** 首次运行后 pi 才会把系统分段持久化,届时用它把分类从差分换成投影口径,只换一次 */
72
+ breakdownProjected?: boolean;
66
73
  buffer: { seq: number; json: string }[];
67
74
  seq: number;
68
75
  /**
@@ -345,36 +352,58 @@ function reapplyDesiredLevel(m: Managed, session: AgentSession): void {
345
352
  if (target !== session.thinkingLevel) session.setThinkingLevel(target as never);
346
353
  }
347
354
 
355
+ /**
356
+ * 新会话页选定模型后的思考档位:与界面显示的生效值同一规则(每模型覆盖 → 设置默认 → medium,再按模型能力钳制)。
357
+ * SDK 切换模型时会沿用会话原来的档位,新会话不能沿用:那个档位是 SDK 按它自己的初始模型定的。
358
+ */
359
+ function applyNewSessionThinking(m: Managed, session: AgentSession): void {
360
+ const model = session.model;
361
+ if (!model) return;
362
+ const settings = getSettingsManager(m.cwd);
363
+ const level = newSessionThinkingLevel({
364
+ supported: session.getAvailableThinkingLevels() as unknown as string[],
365
+ modelOverride: settings.getModelThinkingLevel(model.provider, model.id),
366
+ defaultLevel: settings.getDefaultThinkingLevel(),
367
+ });
368
+ if (level !== session.thinkingLevel) session.setThinkingLevel(level as never);
369
+ }
370
+
348
371
  function publishTraj(m: Managed): void {
349
372
  for (const entry of m.ledger.drainDirty()) {
350
373
  publish(m, { type: "traj", entry, ts: Date.now() });
351
374
  }
352
375
  }
353
376
 
354
- function timingStats(m: Managed): Pick<WebStats, "llmMs" | "toolMs" | "ttftMs" | "ttftSteps" | "decodeMs" | "decodeTokens"> {
377
+ function timingStats(m: Managed): Pick<WebStats, "llmMs" | "toolMs" | "ttftMs" | "ttftSteps" | "decodeMs" | "decodeTokens" | "llmTimedSteps" | "toolTimedCalls" | "decodeSteps"> {
355
378
  let llmMs = 0;
356
379
  let toolMs = 0;
357
380
  let ttftMs = 0;
358
381
  let ttftSteps = 0;
359
382
  let decodeMs = 0;
360
383
  let decodeTokens = 0;
384
+ let llmTimedSteps = 0;
385
+ let toolTimedCalls = 0;
386
+ let decodeSteps = 0;
361
387
  for (const entry of m.ledger.entries) {
362
388
  if (entry.kind === "tool") {
389
+ if (entry.timing?.durationMs !== undefined) toolTimedCalls++;
363
390
  toolMs += Math.max(0, entry.timing?.durationMs ?? 0);
364
391
  continue;
365
392
  }
366
393
  if (entry.kind !== "message") continue;
394
+ if (entry.timing?.durationMs !== undefined) llmTimedSteps++;
367
395
  llmMs += Math.max(0, entry.timing?.durationMs ?? 0);
368
396
  if (entry.timing?.ttftMs !== undefined) {
369
397
  ttftMs += Math.max(0, entry.timing.ttftMs);
370
398
  ttftSteps += 1;
371
399
  }
372
- if (entry.timing?.decodeMs !== undefined && entry.tokens?.output !== undefined) {
373
- decodeMs += Math.max(0, entry.timing.decodeMs);
400
+ if ((entry.timing?.decodeMs ?? 0) > 0 && entry.tokens?.output !== undefined) {
401
+ decodeSteps++;
402
+ decodeMs += Math.max(0, entry.timing?.decodeMs ?? 0);
374
403
  decodeTokens += Math.max(0, entry.tokens.output);
375
404
  }
376
405
  }
377
- return { llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens };
406
+ return { llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens, llmTimedSteps, toolTimedCalls, decodeSteps };
378
407
  }
379
408
 
380
409
  function sessionStats(m: Managed): WebStats | null {
@@ -411,7 +440,7 @@ function coldSessionStats(m: Managed): WebStats {
411
440
  cost += Number(usage.cost?.total ?? 0);
412
441
  };
413
442
  for (const entry of m.sm.getEntries() as any[]) {
414
- if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) addUsage(entry.usage);
443
+ if ((entry.type === "usage" || entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) addUsage(entry.usage);
415
444
  if (entry.type !== "message" || !entry.message) continue;
416
445
  totalMessages += 1;
417
446
  const message = entry.message;
@@ -493,7 +522,14 @@ function translate(m: Managed, evt: AgentSessionEvent): void {
493
522
  // the browser receives the durable entry ID without reordering later tool events.
494
523
  deferPublish(m, () => {
495
524
  const msg = projectMessage(m, raw);
496
- if (msg.role === "assistant") msg.endedAt = now;
525
+ if (msg.role === "assistant") {
526
+ msg.endedAt = now;
527
+ // 首次回复结束后 pi 已把系统分段写进会话文件,此时分类能读到投影(比 systemPrompt 差分精确)
528
+ if (!m.breakdownProjected) {
529
+ m.breakdownProjected = true;
530
+ publishContextBreakdown(m);
531
+ }
532
+ }
497
533
  const usage = usageEvent(m);
498
534
  return usage
499
535
  ? [{ type: "message", message: msg, phase: "end", ts: now }, usage]
@@ -579,6 +615,7 @@ function translate(m: Managed, evt: AgentSessionEvent): void {
579
615
  ts: now,
580
616
  });
581
617
  publishUsage(m);
618
+ publishContextBreakdown(m);
582
619
  if ((evt as any).reason === "manual") {
583
620
  publish(m, { type: "status", isStreaming: isManagedBusy(m), state: isManagedBusy(m) ? "running" : "idle", ts: now });
584
621
  if (m.resourceReloadPending) queueMicrotask(() => void reloadManagedResources(m));
@@ -592,8 +629,8 @@ function translate(m: Managed, evt: AgentSessionEvent): void {
592
629
  else publish(m, { type: "model", thinkingLevel: (evt as any).level, ts: now });
593
630
  break;
594
631
  case "auto_retry_start":
595
- // 重试是流程内通知:发一条结构化 retry 事件(界面聚合成「重试 n/max」一行),
596
- // 不再走 error 通道,免得每次尝试都多出一条报错。
632
+ // 重试是流程内通知:发一条结构化 retry 事件(界面聚合成「重试 n/max」一行),
633
+ // 不再走 error 通道,免得每次尝试都多出一条报错。
597
634
  publish(m, {
598
635
  type: "retry",
599
636
  attempt: Number((evt as any).attempt) || 0,
@@ -602,6 +639,13 @@ function translate(m: Managed, evt: AgentSessionEvent): void {
602
639
  ts: now,
603
640
  });
604
641
  break;
642
+ case "auto_retry_end":
643
+ // 重试收尾:SDK 成功/最终失败/被中止都会发这条,而重试成功后的这轮对话
644
+ // agent_settled 之前界面上还留着「重试 1/5 terminated」那一行(用户看到的
645
+ // 就是「已经恢复了还显示错误」)。这里显式发一条清除事件,重试区间一结束
646
+ // 就收掉这行;真失败时紧随其后的 assistant error 消息会照常出错误块。
647
+ publish(m, { type: "retry_end", attempt: Number((evt as any).attempt) || 0, ts: now });
648
+ break;
605
649
  default:
606
650
  break;
607
651
  }
@@ -733,87 +777,99 @@ function loadContextResources(cwd: string, loader: AgentSession["resourceLoader"
733
777
  ];
734
778
  }
735
779
 
780
+ const SKILLS_SECTION_START = "The following skills provide specialized instructions";
781
+ const SKILLS_SECTION_END = "</available_skills>";
782
+
736
783
  /**
737
- * 上下文占用的服务端估算分项。systemPrompt 是整段拼接(正文 + memory + skills + 工具列表),
738
- * 所以 Memory / Skills 从资源加载器与 systemPrompt 的资源段差分得出:
739
- * SDK 的拼接顺序固定为 正文 → context files → skills → 工具列表,按内容定位切块。
784
+ * 当前上下文的字符分类,优先读 SDK 投影中的系统分段、工具 schema 与摘要。
785
+ * 旧客户端仍可读取字符/4 的估算字段;新客户端仅将字符数作为分类数据。
786
+ * 没有持久化系统消息且尚未启动的会话,资源来自发现缓存并标记 cold。
740
787
  */
741
- async function buildContextBreakdown(
742
- cwd: string,
743
- session: AgentSession | null,
744
- resources: ContextResource[],
745
- ): Promise<ContextBreakdown> {
788
+ async function buildContextBreakdown(m: Managed, resources: ContextResource[]): Promise<ContextBreakdown> {
789
+ const session = m.session;
746
790
  const breakdown: ContextBreakdown = {
747
791
  systemPrompt: 0, systemTools: 0, customTools: 0, memory: 0, skills: 0, compacted: 0, autoCompactBuffer: 0,
792
+ ...(session ? {} : { cold: true }),
748
793
  };
749
- if (!session) return breakdown;
794
+ // Memory:AGENTS.md 等注入文件(contextResources 的 project 部分)
795
+ const memoryText = resources.filter((r) => r.source === "project").map((r) => r.content).join("\n\n");
796
+ // 字符分项只读取当前投影中的内容。历史压缩摘要、未激活工具不混入当前统计。
797
+ const characters = { systemPrompt: 0, systemTools: 0, customTools: 0, memory: 0, skills: 0, compacted: 0 };
750
798
  try {
751
- const prompt = session.systemPrompt ?? "";
752
- // Memory:AGENTS.md 等注入文件(contextResources 的 project 部分)
753
- const memoryText = resources.filter((r) => r.source === "project").map((r) => r.content).join("\n\n");
754
- breakdown.memory = estimateTokensOf(memoryText);
755
- // Skills:从资源加载器取技能清单,按 SDK 的 formatSkillsForPrompt 段落定位。
756
- // 该段落没有 Markdown 标题(原文以 "The following skills provide specialized instructions…" 开头,
757
- // 结尾是 </available_skills>),以前按 "## Skills" 找永远找不到,技能段一直是 0。
758
- try {
759
- const { skills } = session.resourceLoader.getSkills();
760
- if (skills.length && prompt) {
761
- const start = prompt.indexOf("The following skills provide specialized instructions");
762
- const end = prompt.indexOf("</available_skills>");
763
- if (start >= 0 && end > start) breakdown.skills = estimateTokensOf(prompt.slice(start, end + "</available_skills>".length));
799
+ const messages = m.sm.buildSessionProjection().messages as any[];
800
+ const system = messages.findLast((message) => message.role === "system");
801
+ if (system) {
802
+ const contentChars = typeof system.content === "string" ? system.content.length : textOf(system.content).length;
803
+ characters.memory = system.sections?.project_context?.length ?? 0;
804
+ characters.skills = system.sections?.skills?.length ?? 0;
805
+ characters.systemPrompt = contentChars + Object.entries(system.sections ?? {})
806
+ .filter(([key]) => key !== "project_context" && key !== "skills")
807
+ .reduce((sum, [, value]) => sum + (typeof value === "string" ? value.length : 0), 0);
808
+ const builtinNames = new Set(session
809
+ ? session.getAllTools().filter((tool: any) => tool.sourceInfo?.source === "builtin").map((tool) => tool.name)
810
+ : ["read", "bash", "edit", "write", "grep", "find", "ls"]);
811
+ for (const tool of system.toolsAdded ?? []) {
812
+ characters[builtinNames.has(tool.name) ? "systemTools" : "customTools"] += JSON.stringify(tool).length;
764
813
  }
765
- } catch {
766
- /* loader 不可用则跳过 */
767
- }
768
- // System Prompt 正文 = 全文减去资源段与 skills 段的近似差分,下限 0
769
- const estimatedSections = breakdown.memory + breakdown.skills;
770
- breakdown.systemPrompt = Math.max(0, estimateTokensOf(prompt) - estimatedSections);
771
- // 工具:builtin 与扩展注册的分开统计(schema + description + guidelines 序列化估算)
772
- try {
773
- const tools = session.getAllTools();
774
- for (const tool of tools) {
775
- const size = estimateTokensOf(JSON.stringify({
776
- name: tool.name,
777
- description: tool.description,
778
- parameters: tool.parameters,
779
- promptGuidelines: tool.promptGuidelines,
780
- }));
781
- if ((tool as { sourceInfo?: { source?: string } }).sourceInfo?.source === "builtin") breakdown.systemTools += size;
782
- else breakdown.customTools += size;
814
+ delete breakdown.cold;
815
+ } else if (session) {
816
+ const prompt = session.systemPrompt ?? "";
817
+ characters.memory = memoryText.length;
818
+ const start = prompt.indexOf(SKILLS_SECTION_START);
819
+ const end = prompt.indexOf(SKILLS_SECTION_END);
820
+ characters.skills = start >= 0 && end > start ? prompt.slice(start, end + SKILLS_SECTION_END.length).length : 0;
821
+ characters.systemPrompt = Math.max(0, prompt.length - characters.memory - characters.skills);
822
+ const active = new Set(session.getActiveToolNames());
823
+ for (const tool of session.getAllTools().filter((tool) => active.has(tool.name))) {
824
+ const size = JSON.stringify({ name: tool.name, description: tool.description, parameters: tool.parameters }).length;
825
+ characters[(tool as any).sourceInfo?.source === "builtin" ? "systemTools" : "customTools"] += size;
783
826
  }
784
- } catch {
785
- /* getAllTools 不可用则跳过 */
786
- }
787
- // 最新一次压缩的摘要
788
- try {
789
- const entries = m_smEntries(session);
790
- for (let i = entries.length - 1; i >= 0; i -= 1) {
791
- const entry = entries[i] as { type?: string; summary?: string };
792
- if (entry?.type === "compaction" && typeof entry.summary === "string") {
793
- breakdown.compacted = estimateTokensOf(entry.summary);
794
- break;
795
- }
796
- }
797
- } catch {
798
- /* entries 不可用则跳过 */
827
+ } else {
828
+ // 初次启动前只能读到待注入的资源,界面保留 cold 提示。
829
+ characters.memory = memoryText.length;
830
+ const loader = peekResourceLoader(m.cwd) ?? getResourceLoader(m.cwd);
831
+ characters.skills = formatSkillsForPrompt(loader.getSkills().skills).trim().length;
799
832
  }
800
- // auto-compact 预留 = compaction.reserveTokens(启用时)
801
- try {
802
- const settings = await getPiSettings();
803
- if (settings.compaction.enabled) breakdown.autoCompactBuffer = settings.compaction.reserveTokens;
804
- } catch {
805
- /* 设置读取失败则保持 0 */
833
+ for (const message of messages) {
834
+ if (message.role === "compactionSummary" || message.role === "branchSummary") characters.compacted += message.summary?.length ?? 0;
806
835
  }
836
+ breakdown.characters = characters;
837
+ // 保留旧客户端的估算字段,当前客户端直接展示实际字符数。
838
+ for (const key of Object.keys(characters) as Array<keyof typeof characters>) breakdown[key] = Math.ceil(characters[key] / 4);
807
839
  } catch {
808
- /* systemPrompt 不可用则保持 0 */
840
+ /* 投影不可用时不给字符分项,不能从旧 token 估算反推字符数。 */
841
+ }
842
+ // auto-compact 预留读本工作区、当前模型实际使用的设置。
843
+ try {
844
+ const compaction = getSettingsManager(m.cwd).getCompactionSettings(session?.model);
845
+ if (compaction.enabled) breakdown.autoCompactBuffer = compaction.reserveTokens;
846
+ } catch {
847
+ /* 设置读取失败则保持 0 */
809
848
  }
810
849
  return breakdown;
811
850
  }
812
851
 
813
- /** SessionManager 的 getEntries 经由 session 暴露的形态(各版本字段名有差异,这里宽松取) */
814
- function m_smEntries(session: AgentSession): unknown[] {
815
- const s = session as unknown as { sm?: { getEntries?: () => unknown[] }; sessionManager?: { getEntries?: () => unknown[] } };
816
- return s.sm?.getEntries?.() ?? s.sessionManager?.getEntries?.() ?? [];
852
+ /**
853
+ * 分项变了就推一次(会话启动、工具 / 资源重载、压缩之后)。分项以前只随快照下发:
854
+ * 打开冷会话时是 0,之后会话启动了弹窗也一直停在那份 0 上,直到刷新页面。
855
+ * 同一轮里多次触发(启动时 publishTools + publishResources)合并成一次。
856
+ */
857
+ function publishContextBreakdown(m: Managed): void {
858
+ if (m.breakdownQueued || m.disposed) return;
859
+ m.breakdownQueued = true;
860
+ queueMicrotask(() => {
861
+ m.breakdownQueued = false;
862
+ if (m.disposed) return;
863
+ let resources: ContextResource[] = [];
864
+ try {
865
+ resources = loadContextResources(m.cwd, m.session?.resourceLoader);
866
+ } catch {
867
+ /* 读不到注入文件就按空算 Memory */
868
+ }
869
+ void buildContextBreakdown(m, resources).then((breakdown) => {
870
+ if (!m.disposed) publish(m, { type: "context_breakdown", breakdown, ts: Date.now() });
871
+ }, () => undefined);
872
+ });
817
873
  }
818
874
 
819
875
  /** resources 已经算过(快照)就传进来,免得资源发现结果再读一遍 */
@@ -877,7 +933,7 @@ export async function ensureSession(m: Managed): Promise<AgentSession> {
877
933
  installSessionPolicy(m, session);
878
934
  applyToolPolicy(m, session);
879
935
  session.subscribe((evt) => { if (!m.disposed) translate(m, evt); });
880
- const ui = createExtensionUiBridge({ publish: (evt) => publish(m, evt), hasViewers: () => !m.disposed && m.subscribers.size > 0 });
936
+ const ui = createExtensionUiBridge({ publish: (evt) => publish(m, evt), hasViewers: () => !m.disposed && hasViewers(m) });
881
937
  m.ui = ui;
882
938
  await session.bindExtensions({
883
939
  mode: "rpc",
@@ -989,6 +1045,7 @@ function shutdownSession(session: AgentSession): Promise<void> {
989
1045
 
990
1046
  function publishTools(m: Managed, session: AgentSession): void {
991
1047
  publish(m, { type: "tools", active: listActiveTools(session), all: listAllToolNames(session), toolPreset: m.toolPreset, customActiveTools: m.customActiveTools ?? null, ts: Date.now() });
1048
+ publishContextBreakdown(m);
992
1049
  }
993
1050
 
994
1051
  /** SDK reload 自动启用扩展工具,注册新工具也会刷新全集;统一在激活边界收敛到服务端选择。 */
@@ -1086,13 +1143,13 @@ export async function disposeSessionPath(sessionPath: string): Promise<void> {
1086
1143
  export function reap(): void {
1087
1144
  const now = Date.now();
1088
1145
  for (const m of [...sessions.values()]) {
1089
- if (m.subscribers.size > 0 || now - m.lastActive <= IDLE_MS) continue;
1146
+ if (hasViewers(m) || now - m.lastActive <= IDLE_MS) continue;
1090
1147
  if (!m.creating && !isManagedBusy(m)) disposeSession(m);
1091
1148
  }
1092
1149
  const active = [...sessions.values()].filter((m) => m.session);
1093
1150
  if (active.length > MAX_ACTIVE) {
1094
1151
  const evictable = active
1095
- .filter((m) => !m.creating && !isManagedBusy(m) && m.subscribers.size === 0)
1152
+ .filter((m) => !m.creating && !isManagedBusy(m) && !hasViewers(m))
1096
1153
  .sort((a, b) => a.lastActive - b.lastActive);
1097
1154
  for (const m of evictable.slice(0, active.length - MAX_ACTIVE)) disposeSession(m);
1098
1155
  }
@@ -1166,6 +1223,7 @@ function collectResources(m: Managed): ResourceBundle {
1166
1223
 
1167
1224
  export function publishResources(m: Managed): void {
1168
1225
  publish(m, { type: "resources", ...collectResources(m), ts: Date.now() });
1226
+ publishContextBreakdown(m);
1169
1227
  }
1170
1228
 
1171
1229
  /**
@@ -1177,11 +1235,7 @@ async function reloadManagedResources(m: Managed): Promise<boolean> {
1177
1235
  m.resourceReloadPending = false;
1178
1236
  try {
1179
1237
  await m.session.reload();
1180
- // SDK 的 reload 不会换当前模型对象:models.json 改了 baseUrl / contextWindow 后,
1181
- // 会话手里还是旧对象。按 provider/id 从(已就地刷新的)共享 runtime 取一次新的。
1182
- const current = m.session.model;
1183
- const fresh = current && m.session.modelRuntime.getModel(current.provider, current.id);
1184
- if (fresh && fresh !== current) m.session.agent.state.model = fresh;
1238
+ swapCurrentModelObject(m.session);
1185
1239
  return true;
1186
1240
  } catch (err) {
1187
1241
  publish(m, { type: "error", message: `重载扩展失败:${String((err as Error)?.message ?? err)}`, ts: Date.now() });
@@ -1191,6 +1245,32 @@ async function reloadManagedResources(m: Managed): Promise<boolean> {
1191
1245
  }
1192
1246
  }
1193
1247
 
1248
+ /**
1249
+ * SDK 的 reload 不会换当前模型对象:models.json 改了 baseUrl / contextWindow 后,会话手里还是旧对象。
1250
+ * 按 provider/id 从(已就地刷新的)共享 runtime 取一次新的。
1251
+ */
1252
+ function swapCurrentModelObject(session: AgentSession): void {
1253
+ const current = session.model;
1254
+ const fresh = current && session.modelRuntime.getModel(current.provider, current.id);
1255
+ if (fresh && fresh !== current) session.agent.state.model = fresh;
1256
+ }
1257
+
1258
+ /**
1259
+ * 共享目录刷新后,给正在用这个模型的会话换上新模型对象(上下文窗口等字段跟着变),再推一次用量让圆环当场更新。
1260
+ * 运行中的会话也换:SDK 在扩展注册 provider 时同样会中途替换 agent.state.model,已发出的请求不受影响。
1261
+ */
1262
+ export function refreshSessionModels(provider: string, id: string): number {
1263
+ let n = 0;
1264
+ for (const m of sessions.values()) {
1265
+ const current = m.session?.model;
1266
+ if (!m.session || current?.provider !== provider || current.id !== id) continue;
1267
+ swapCurrentModelObject(m.session);
1268
+ publishUsage(m);
1269
+ n += 1;
1270
+ }
1271
+ return n;
1272
+ }
1273
+
1194
1274
  export async function reloadSessionsForCwd(cwd?: string): Promise<number> {
1195
1275
  const key = (p: string) => (process.platform === "win32" ? p.replace(/\\/g, "/").toLowerCase() : p.replace(/\\/g, "/"));
1196
1276
  let n = 0;
@@ -1198,7 +1278,7 @@ export async function reloadSessionsForCwd(cwd?: string): Promise<number> {
1198
1278
  if (cwd && key(m.cwd) !== key(cwd)) continue;
1199
1279
  if (!m.session) {
1200
1280
  // 冷会话没有扩展运行时可重建,但页面可能开着:把新清单(模板 / 技能 / 信任)推过去
1201
- if (m.subscribers.size > 0) publishResources(m);
1281
+ if (hasViewers(m)) publishResources(m);
1202
1282
  continue;
1203
1283
  }
1204
1284
  if (m.creating || isManagedBusy(m)) {
@@ -1294,42 +1374,13 @@ export async function buildSnapshot(m: Managed): Promise<WebSnapshot> {
1294
1374
  const usage = session.getContextUsage() ?? null;
1295
1375
  contextUsage = usage ? { tokens: usage.tokens, contextWindow: usage.contextWindow, percent: usage.percent } : null;
1296
1376
  } else if (model) {
1297
- // 冷会话没有 SDK 实例:用最后一轮 assistant 用量(input+cache+output ≈ 该轮后的
1298
- // 上下文规模)除以模型窗口估算,避免上下文计量永远显示 0% / —
1377
+ // 冷会话也使用当前分支投影,包含最后一次 API 用量之后的用户消息和工具输出。
1299
1378
  try {
1300
1379
  const rt = await getModelRuntime();
1301
1380
  const modelMeta = (rt.getModels(model.provider) as any[]).find((x) => String(x.id) === model.id);
1302
- const window = Number(modelMeta?.contextWindow ?? 0);
1303
- let lastTurnTokens = 0;
1304
- // 与 SDK getContextUsage 同一条规则:最近一次压缩之后还没有新的助手回复时,上下文大小未知(不能拿压缩前的用量充数)
1305
- const entries = m.sm.getEntries() as Array<{ type?: string; message?: { role?: string; stopReason?: string } }>;
1306
- let compactedSinceLastAssistant = false;
1307
- for (let i = entries.length - 1; i >= 0; i--) {
1308
- const e = entries[i];
1309
- if (e.type === "compaction" || e.type === "branch_summary") {
1310
- compactedSinceLastAssistant = true;
1311
- break;
1312
- }
1313
- if (e.type === "message" && e.message?.role === "assistant" && e.message.stopReason !== "error" && e.message.stopReason !== "aborted") break;
1314
- }
1315
- for (let i = messages.length - 1; i >= 0 && !compactedSinceLastAssistant; i--) {
1316
- const msg = messages[i];
1317
- if (msg.role === "assistant" && msg.usage) {
1318
- lastTurnTokens =
1319
- Number(msg.usage.input ?? 0) +
1320
- Number(msg.usage.cacheRead ?? 0) +
1321
- Number(msg.usage.cacheWrite ?? 0) +
1322
- Number(msg.usage.output ?? 0);
1323
- break;
1324
- }
1325
- }
1326
- if (window > 0 && lastTurnTokens > 0) {
1327
- contextUsage = { tokens: lastTurnTokens, contextWindow: window, percent: (lastTurnTokens / window) * 100 };
1328
- } else if (window > 0) {
1329
- contextUsage = { tokens: null, contextWindow: window, percent: null };
1330
- }
1381
+ contextUsage = projectedContextUsage(m.sm.buildSessionProjection(), m.sm.getBranch(), Number(modelMeta?.contextWindow ?? 0));
1331
1382
  } catch {
1332
- /* 模型目录不可用则维持 null(UI 显示 0%) */
1383
+ /* 模型或投影不可用时用量未知。 */
1333
1384
  }
1334
1385
  }
1335
1386
 
@@ -1359,7 +1410,7 @@ export async function buildSnapshot(m: Managed): Promise<WebSnapshot> {
1359
1410
  // 服务端可得的上下文分项(估算;失败不影响快照)
1360
1411
  let contextBreakdown: ContextBreakdown | undefined;
1361
1412
  try {
1362
- contextBreakdown = await buildContextBreakdown(m.cwd, m.session, contextResources);
1413
+ contextBreakdown = await buildContextBreakdown(m, contextResources);
1363
1414
  } catch {
1364
1415
  contextBreakdown = undefined;
1365
1416
  }
@@ -1446,6 +1497,18 @@ export function unsubscribe(m: Managed, send: (seq: number, json: string) => voi
1446
1497
  m.subscribers.delete(send);
1447
1498
  }
1448
1499
 
1500
+ /**
1501
+ * 长轮询(快速隧道)的一次请求结束时调用:下一次请求到来前的这段空档仍算有人在看,
1502
+ * 否则扩展弹窗会被当成无人值守直接取默认值,会话也可能被当成没人开着回收掉。
1503
+ */
1504
+ export function holdViewer(m: Managed, ms: number): void {
1505
+ m.viewerGraceUntil = Math.max(m.viewerGraceUntil ?? 0, Date.now() + ms);
1506
+ }
1507
+
1508
+ function hasViewers(m: Managed): boolean {
1509
+ return m.subscribers.size > 0 || (m.viewerGraceUntil ?? 0) > Date.now();
1510
+ }
1511
+
1449
1512
  // ---------- 命令执行 ----------
1450
1513
 
1451
1514
  export interface CommandResult {
@@ -1557,6 +1620,8 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
1557
1620
  if (!model) return { ok: false, error: `unknown model ${cmd.provider}/${cmd.modelId}` };
1558
1621
  await session.setModel(model, { persist: false });
1559
1622
  reapplyDesiredLevel(m, session);
1623
+ // 新会话页选的模型(fresh):思考档位按新会话的规则定;之后显式选的档位仍会覆盖
1624
+ if (cmd.fresh === true && !m.desiredThinkingLevel) applyNewSessionThinking(m, session);
1560
1625
  publishModelState(m, session);
1561
1626
  return { ok: true, data: { thinkingLevel: session.thinkingLevel, thinkingLevels: session.getAvailableThinkingLevels() } };
1562
1627
  }
@@ -1669,8 +1734,17 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
1669
1734
  }
1670
1735
  case "reload": {
1671
1736
  if (isManagedBusy(m)) return { ok: false, error: "session is busy" };
1737
+ // /reload:先就地刷新共享的模型目录(models.json / auth.json 可能是手改的);其它会话手里的
1738
+ // 模型对象跟着换新,忙着的等本轮结束再换;本会话重载资源后再换一次
1739
+ await refreshModelRuntime();
1740
+ for (const other of sessions.values()) {
1741
+ if (other === m || !other.session) continue;
1742
+ if (isManagedBusy(other)) other.resourceReloadPending = true;
1743
+ else swapCurrentModelObject(other.session);
1744
+ }
1672
1745
  const session = await ensureSession(m);
1673
1746
  await session.reload();
1747
+ swapCurrentModelObject(session);
1674
1748
  return { ok: true };
1675
1749
  }
1676
1750
  case "fork": {
@@ -0,0 +1,113 @@
1
+ /**
2
+ * 会话事件的长轮询通道(浏览器端)。
3
+ *
4
+ * Cloudflare 快速隧道(远程连接的公网入口)不支持 SSE:流式响应被攒成 128KB 一块才转发,
5
+ * 不满一块的尾巴一直卡着——打开会话等不到完整快照(一直转圈),聊天也不流式。
6
+ * 隧道下改成一次次短请求:服务端把这一批事件发完就结束响应(结束时隧道一定吐干净),
7
+ * 浏览器带着 Last-Event-ID 立刻再来一次,漏掉的事件由服务端从环形缓冲补发。
8
+ * 线上格式仍是 SSE 文本,服务端两种模式共用一套写法。
9
+ */
10
+
11
+ /** 只有快速隧道需要轮询;局域网、本机、用户自己的反向代理都照旧走 SSE */
12
+ export function usesEventPolling(hostname: string): boolean {
13
+ return hostname.toLowerCase().endsWith(".trycloudflare.com");
14
+ }
15
+
16
+ export interface SseFrame {
17
+ id?: string;
18
+ event?: string;
19
+ data: string;
20
+ }
21
+
22
+ /** 解析一段完整的 SSE 文本(只认本项目用到的 id / event / data,注释行跳过) */
23
+ export function parseSseText(text: string): SseFrame[] {
24
+ const frames: SseFrame[] = [];
25
+ for (const block of text.split(/\r?\n\r?\n/)) {
26
+ const frame: SseFrame = { data: "" };
27
+ const data: string[] = [];
28
+ for (const line of block.split(/\r?\n/)) {
29
+ if (!line || line.startsWith(":")) continue;
30
+ const colon = line.indexOf(":");
31
+ const field = colon === -1 ? line : line.slice(0, colon);
32
+ let value = colon === -1 ? "" : line.slice(colon + 1);
33
+ if (value.startsWith(" ")) value = value.slice(1);
34
+ if (field === "data") data.push(value);
35
+ else if (field === "id") frame.id = value;
36
+ else if (field === "event") frame.event = value;
37
+ }
38
+ if (!data.length) continue;
39
+ frame.data = data.join("\n");
40
+ frames.push(frame);
41
+ }
42
+ return frames;
43
+ }
44
+
45
+ export interface EventPollHandlers {
46
+ /** 一次请求成功返回(断开后恢复时也会再调) */
47
+ onOpen(): void;
48
+ onMessage(data: string): void;
49
+ /** 一次请求失败;之后按退避自动重试 */
50
+ onError(): void;
51
+ }
52
+
53
+ export interface EventPollOptions {
54
+ fetch?: typeof fetch;
55
+ /** 第 n 次连续失败后等多久再试 */
56
+ backoffMs?: (failures: number) => number;
57
+ }
58
+
59
+ const defaultBackoff = (failures: number) => Math.min(2000 * 2 ** (failures - 1), 30_000);
60
+
61
+ export function pollEventStream(url: string, handlers: EventPollHandlers, { fetch: fetchImpl = fetch, backoffMs = defaultBackoff }: EventPollOptions = {}): { close(): void } {
62
+ const controller = new AbortController();
63
+ let closed = false;
64
+ let lastEventId: string | undefined;
65
+ let open = false;
66
+ let failures = 0;
67
+ const pollUrl = `${url}${url.includes("?") ? "&" : "?"}poll=1`;
68
+
69
+ const sleep = (ms: number) => new Promise<void>((resolve) => {
70
+ const timer = setTimeout(resolve, ms);
71
+ controller.signal.addEventListener("abort", () => { clearTimeout(timer); resolve(); }, { once: true });
72
+ });
73
+
74
+ void (async () => {
75
+ while (!closed) {
76
+ let frames: SseFrame[];
77
+ try {
78
+ const response = await fetchImpl(pollUrl, {
79
+ headers: lastEventId ? { "Last-Event-ID": lastEventId } : {},
80
+ cache: "no-store",
81
+ signal: controller.signal,
82
+ });
83
+ if (!response.ok) throw new Error(`events poll failed (${response.status})`);
84
+ frames = parseSseText(await response.text());
85
+ } catch {
86
+ if (closed) return;
87
+ open = false;
88
+ handlers.onError();
89
+ failures += 1;
90
+ await sleep(backoffMs(failures));
91
+ continue;
92
+ }
93
+ if (closed) return;
94
+ failures = 0;
95
+ if (!open) {
96
+ open = true;
97
+ handlers.onOpen();
98
+ }
99
+ for (const frame of frames) {
100
+ if (closed) return;
101
+ handlers.onMessage(frame.data);
102
+ if (frame.id !== undefined) lastEventId = frame.id;
103
+ }
104
+ }
105
+ })();
106
+
107
+ return {
108
+ close() {
109
+ closed = true;
110
+ controller.abort();
111
+ },
112
+ };
113
+ }