@youweichen/pi-harness 0.0.0-stage → 1.0.0

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 (455) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -2
  3. package/bin/pi-harness.mjs +1819 -0
  4. package/deploy/com.youweichen.pi-harness.plist +48 -0
  5. package/deploy/nginx-subpath.conf +88 -0
  6. package/deploy/pi-harness-task.xml +71 -0
  7. package/deploy/pi-harness.service +31 -0
  8. package/dist/server/agent-service.js +3051 -0
  9. package/dist/server/app-version.js +19 -0
  10. package/dist/server/attachments.js +526 -0
  11. package/dist/server/auth-file.js +61 -0
  12. package/dist/server/bg-servers.js +154 -0
  13. package/dist/server/client-eviction.js +19 -0
  14. package/dist/server/client-id.js +9 -0
  15. package/dist/server/client-state.js +202 -0
  16. package/dist/server/codemode-image-routes.js +36 -0
  17. package/dist/server/codemode-presentation.js +19 -0
  18. package/dist/server/component-updates.js +135 -0
  19. package/dist/server/context-breakdown.js +56 -0
  20. package/dist/server/control-socket.js +177 -0
  21. package/dist/server/conversation.js +65 -0
  22. package/dist/server/dir-browser.js +77 -0
  23. package/dist/server/editor-snapshot.js +38 -0
  24. package/dist/server/ensure-bash.js +80 -0
  25. package/dist/server/extension-display.js +23 -0
  26. package/dist/server/extensions-catalog.js +64 -0
  27. package/dist/server/extensions-model.js +46 -0
  28. package/dist/server/extensions-routes.js +255 -0
  29. package/dist/server/extensions-worker.js +187 -0
  30. package/dist/server/files-service.js +795 -0
  31. package/dist/server/index.js +1029 -0
  32. package/dist/server/markdown-images.js +42 -0
  33. package/dist/server/model-admin.js +801 -0
  34. package/dist/server/model-config-merge.js +32 -0
  35. package/dist/server/native-mcp-config.js +103 -0
  36. package/dist/server/native-mcp-presentation.js +12 -0
  37. package/dist/server/native-prompt-template.js +12 -0
  38. package/dist/server/native-tools.js +32 -0
  39. package/dist/server/node-sources.js +142 -0
  40. package/dist/server/node-workbench.js +668 -0
  41. package/dist/server/origin-policy.js +50 -0
  42. package/dist/server/patch-node-pty.js +96 -0
  43. package/dist/server/pi-environment.js +203 -0
  44. package/dist/server/plan/extension.js +74 -0
  45. package/dist/server/plan/progress.js +90 -0
  46. package/dist/server/plan/settings.js +41 -0
  47. package/dist/server/plan/state.js +113 -0
  48. package/dist/server/plugin-facilities.js +304 -0
  49. package/dist/server/plugin-updater.js +245 -0
  50. package/dist/server/plugins.js +712 -0
  51. package/dist/server/private-file.js +28 -0
  52. package/dist/server/process-utils.js +112 -0
  53. package/dist/server/prompt-delivery.js +80 -0
  54. package/dist/server/protocol-version.js +11 -0
  55. package/dist/server/protocol.js +6 -0
  56. package/dist/server/provider-auth.js +99 -0
  57. package/dist/server/query-cache.js +45 -0
  58. package/dist/server/recovery-state.js +37 -0
  59. package/dist/server/scm.js +459 -0
  60. package/dist/server/security-headers.js +24 -0
  61. package/dist/server/serialize.js +254 -0
  62. package/dist/server/session-file-read.js +225 -0
  63. package/dist/server/session-record-worker.js +21 -0
  64. package/dist/server/session-reload.js +13 -0
  65. package/dist/server/session-tree-controller.js +250 -0
  66. package/dist/server/session-tree.js +191 -0
  67. package/dist/server/settings-service.js +53 -0
  68. package/dist/server/skills-service.js +87 -0
  69. package/dist/server/slash-commands.js +280 -0
  70. package/dist/server/sqlite-preview.js +58 -0
  71. package/dist/server/sqlite-query.js +67 -0
  72. package/dist/server/sqlite-worker.js +12 -0
  73. package/dist/server/system-prompt-files.js +158 -0
  74. package/dist/server/system-prompt-routes.js +40 -0
  75. package/dist/server/system-prompt-view.js +49 -0
  76. package/dist/server/task-progress.js +233 -0
  77. package/dist/server/terminals.js +833 -0
  78. package/dist/server/text-sniff.js +269 -0
  79. package/dist/server/thinking-timing.js +91 -0
  80. package/dist/server/token-auth.js +37 -0
  81. package/dist/server/tool-output.js +97 -0
  82. package/dist/server/tool-text-incidents.js +82 -0
  83. package/dist/server/tool-text.js +67 -0
  84. package/dist/server/uploads.js +112 -0
  85. package/dist/server/user-attachments.js +96 -0
  86. package/dist/server/webui-context.js +257 -0
  87. package/dist/server/wiki-links.js +136 -0
  88. package/dist/server/wiki-pdf-worker.js +23 -0
  89. package/dist/server/wiki-pdf.js +36 -0
  90. package/dist/server/wiki-routes.js +177 -0
  91. package/dist/server/wiki-service.js +511 -0
  92. package/electron/main.mjs +607 -0
  93. package/extensions/webui.ts +192 -0
  94. package/package.json +122 -4
  95. package/web/dist/apple-touch-icon.png +0 -0
  96. package/web/dist/assets/NodeWorkbench-C4U1PldE.js +3 -0
  97. package/web/dist/assets/TerminalPanel-CWh6vw_N.js +2 -0
  98. package/web/dist/assets/abnfDiagram-VCTEODGH-D5Ti0Kpa.js +1 -0
  99. package/web/dist/assets/arc-DKJilLd6.js +1 -0
  100. package/web/dist/assets/architectureDiagram-5GKGNRK7-Cf0w38g-.js +36 -0
  101. package/web/dist/assets/blockDiagram-I7D4REHJ-BOTLWLIL.js +129 -0
  102. package/web/dist/assets/c4Diagram-7LVT6UL2-CLCCoJs3.js +38 -0
  103. package/web/dist/assets/channel-D8Hd7T4P.js +1 -0
  104. package/web/dist/assets/chunk-2Q5K7J3B-BHjga2Ge.js +1 -0
  105. package/web/dist/assets/chunk-5VM5RSS4-CQJ5VfDZ.js +15 -0
  106. package/web/dist/assets/chunk-F27PBJKO-C67qoPJH.js +1 -0
  107. package/web/dist/assets/chunk-IMKFNOWR-CgXXGwI8.js +231 -0
  108. package/web/dist/assets/chunk-JWPE2WC7-D0RPyByP.js +1 -0
  109. package/web/dist/assets/chunk-POPQ4Y6H-BeYC9s9x.js +1 -0
  110. package/web/dist/assets/chunk-SVP7TREG-eQPl4UBQ.js +88 -0
  111. package/web/dist/assets/chunk-TICWLB2K-DT73arPU.js +206 -0
  112. package/web/dist/assets/chunk-XXDRQBXY-B4FwQcP-.js +1 -0
  113. package/web/dist/assets/classDiagram-ZZMXUADV-B_btgYRB.js +1 -0
  114. package/web/dist/assets/classDiagram-v2-VYDZK3BY-B_btgYRB.js +1 -0
  115. package/web/dist/assets/cose-bilkent-JH36ORCC-Bnn5nlQN.js +1 -0
  116. package/web/dist/assets/cynefin-OW5HDTMX-CStWX9kE.js +178 -0
  117. package/web/dist/assets/cynefinDiagram-5FMLGOSQ-BMVHC03f.js +62 -0
  118. package/web/dist/assets/cytoscape.esm-Bch-eiPH.js +331 -0
  119. package/web/dist/assets/dagre-GXQ25YYZ-ClSaO4ml.js +4 -0
  120. package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  121. package/web/dist/assets/diagram-S7CK7UJ4-C43LL08C.js +30 -0
  122. package/web/dist/assets/diagram-UQ7AKVKN-BPVn_ySW.js +41 -0
  123. package/web/dist/assets/diagram-VSXAHHWV-C34FFhRi.js +3 -0
  124. package/web/dist/assets/diagram-VX7I27RA-BRk4bncf.js +24 -0
  125. package/web/dist/assets/diagram-Z3DM3KII-O5hYPLEd.js +24 -0
  126. package/web/dist/assets/ebnfDiagram-PWID7BFC-DVGEBm99.js +1 -0
  127. package/web/dist/assets/erDiagram-RLTQ6QDP-M-BLpTEV.js +99 -0
  128. package/web/dist/assets/flowDiagram-HODETNUW-BwdLopEx.js +168 -0
  129. package/web/dist/assets/ganttDiagram-EL5Y4UJY-DYxx2ppA.js +292 -0
  130. package/web/dist/assets/gitGraphDiagram-WWUBYQGX-DUU0wMYB.js +106 -0
  131. package/web/dist/assets/index-CI2KfVSr.css +10 -0
  132. package/web/dist/assets/index-DQcpggFU.js +275 -0
  133. package/web/dist/assets/infoDiagram-27XIBGKW-CFaW5C0C.js +2 -0
  134. package/web/dist/assets/init-Gi6I4Gst.js +1 -0
  135. package/web/dist/assets/ishikawaDiagram-5VMMS53U--ENP1MTB.js +70 -0
  136. package/web/dist/assets/journeyDiagram-3NMN7TZE-DgnAzSTj.js +139 -0
  137. package/web/dist/assets/kanban-definition-UXKFOSKX-_STZN1vU.js +89 -0
  138. package/web/dist/assets/katex-C5jXJg4s.js +257 -0
  139. package/web/dist/assets/layout-CkUClkXt.js +1 -0
  140. package/web/dist/assets/linear-sYHiUOxa.js +1 -0
  141. package/web/dist/assets/markdown-vNTCZJ6d.js +51 -0
  142. package/web/dist/assets/mermaid.core-B8fKGWsc.js +312 -0
  143. package/web/dist/assets/mindmap-definition-YA3MSWOX-DP0BYOEx.js +96 -0
  144. package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  145. package/web/dist/assets/pegDiagram-XKGWAZYB-DkWTmZre.js +1 -0
  146. package/web/dist/assets/pieDiagram-E7YTZNPT-DfZgMV4u.js +39 -0
  147. package/web/dist/assets/quadrantDiagram-AXDQQJYC-Djl4TT7q.js +7 -0
  148. package/web/dist/assets/railroadDiagram-O6MQD6OU-DbKF6zXn.js +1 -0
  149. package/web/dist/assets/react-BNj3n7jX.js +24 -0
  150. package/web/dist/assets/requirementDiagram-BXWQKSXE-idola5qb.js +84 -0
  151. package/web/dist/assets/sankeyDiagram-P5KCCOFB-KA0hQOle.js +40 -0
  152. package/web/dist/assets/sequenceDiagram-WJ2MYXX4-BXSKvxzP.js +162 -0
  153. package/web/dist/assets/sizeCapture-INFHLROL-C_Ewv3S5.js +1 -0
  154. package/web/dist/assets/stateDiagram-D77RDMKH-DIf5X8oo.js +1 -0
  155. package/web/dist/assets/stateDiagram-v2-MP3YSRHH-DYwe_2mB.js +1 -0
  156. package/web/dist/assets/swimlanes-42K2YHIH-Cx_Rc8M2.js +1 -0
  157. package/web/dist/assets/swimlanesDiagram-VR7AAH4N-52Y8tgRV.js +8 -0
  158. package/web/dist/assets/timeline-definition-24CTP7MA-BRrDhWVz.js +120 -0
  159. package/web/dist/assets/vennDiagram-4TSXK5OY-Ad6G0YwD.js +34 -0
  160. package/web/dist/assets/wardleyDiagram-VM6X3IG4-X6-ENpSi.js +78 -0
  161. package/web/dist/assets/xterm-6GBZ9nXN.css +32 -0
  162. package/web/dist/assets/xterm-D1D2FVe3.js +38 -0
  163. package/web/dist/assets/xychartDiagram-S5SC5T6Z-DhegNS6d.js +7 -0
  164. package/web/dist/brand-mark.svg +5 -0
  165. package/web/dist/favicon-32.png +0 -0
  166. package/web/dist/favicon.svg +5 -0
  167. package/web/dist/fonts/WIKI-FONTS.md +5 -0
  168. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  169. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  170. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  171. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
  172. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
  173. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-600-normal.woff2 +0 -0
  174. package/web/dist/fonts/ibm-plex-sans/LICENSE +93 -0
  175. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  176. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  177. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  178. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
  179. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
  180. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
  181. package/web/dist/fonts/inter/inter-latin-400-normal.woff2 +0 -0
  182. package/web/dist/fonts/inter/inter-latin-500-normal.woff2 +0 -0
  183. package/web/dist/fonts/inter/inter-latin-600-normal.woff2 +0 -0
  184. package/web/dist/fonts/inter/inter-latin-700-normal.woff2 +0 -0
  185. package/web/dist/fonts/inter/inter-latin-800-normal.woff2 +0 -0
  186. package/web/dist/fonts/inter/inter-latin-ext-400-normal.woff2 +0 -0
  187. package/web/dist/fonts/inter/inter-latin-ext-500-normal.woff2 +0 -0
  188. package/web/dist/fonts/inter/inter-latin-ext-600-normal.woff2 +0 -0
  189. package/web/dist/fonts/inter/inter-latin-ext-700-normal.woff2 +0 -0
  190. package/web/dist/fonts/inter/inter-latin-ext-800-normal.woff2 +0 -0
  191. package/web/dist/fonts/jetbrains-mono/LICENSE +93 -0
  192. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-400-normal.woff2 +0 -0
  193. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-500-normal.woff2 +0 -0
  194. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-600-normal.woff2 +0 -0
  195. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
  196. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
  197. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-600-normal.woff2 +0 -0
  198. package/web/dist/fonts/noto-serif-sc/LICENSE +94 -0
  199. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-0.woff2 +0 -0
  200. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-1.woff2 +0 -0
  201. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-10.woff2 +0 -0
  202. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-100.woff2 +0 -0
  203. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-11.woff2 +0 -0
  204. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-12.woff2 +0 -0
  205. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-13.woff2 +0 -0
  206. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-14.woff2 +0 -0
  207. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-15.woff2 +0 -0
  208. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-16.woff2 +0 -0
  209. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-17.woff2 +0 -0
  210. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-18.woff2 +0 -0
  211. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-19.woff2 +0 -0
  212. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-2.woff2 +0 -0
  213. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-20.woff2 +0 -0
  214. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-21.woff2 +0 -0
  215. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-22.woff2 +0 -0
  216. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-23.woff2 +0 -0
  217. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-24.woff2 +0 -0
  218. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-25.woff2 +0 -0
  219. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-26.woff2 +0 -0
  220. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-27.woff2 +0 -0
  221. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-28.woff2 +0 -0
  222. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-29.woff2 +0 -0
  223. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-3.woff2 +0 -0
  224. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-30.woff2 +0 -0
  225. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-31.woff2 +0 -0
  226. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-32.woff2 +0 -0
  227. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-33.woff2 +0 -0
  228. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-34.woff2 +0 -0
  229. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-35.woff2 +0 -0
  230. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-36.woff2 +0 -0
  231. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-37.woff2 +0 -0
  232. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-38.woff2 +0 -0
  233. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-39.woff2 +0 -0
  234. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-4.woff2 +0 -0
  235. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-40.woff2 +0 -0
  236. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-41.woff2 +0 -0
  237. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-42.woff2 +0 -0
  238. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-43.woff2 +0 -0
  239. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-44.woff2 +0 -0
  240. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-45.woff2 +0 -0
  241. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-46.woff2 +0 -0
  242. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-47.woff2 +0 -0
  243. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-48.woff2 +0 -0
  244. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-49.woff2 +0 -0
  245. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-5.woff2 +0 -0
  246. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-50.woff2 +0 -0
  247. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-51.woff2 +0 -0
  248. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-52.woff2 +0 -0
  249. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-53.woff2 +0 -0
  250. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-54.woff2 +0 -0
  251. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-55.woff2 +0 -0
  252. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-56.woff2 +0 -0
  253. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-57.woff2 +0 -0
  254. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-58.woff2 +0 -0
  255. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-59.woff2 +0 -0
  256. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-6.woff2 +0 -0
  257. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-60.woff2 +0 -0
  258. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-61.woff2 +0 -0
  259. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-62.woff2 +0 -0
  260. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-63.woff2 +0 -0
  261. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-64.woff2 +0 -0
  262. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-65.woff2 +0 -0
  263. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-66.woff2 +0 -0
  264. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-67.woff2 +0 -0
  265. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-68.woff2 +0 -0
  266. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-69.woff2 +0 -0
  267. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-7.woff2 +0 -0
  268. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-70.woff2 +0 -0
  269. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-71.woff2 +0 -0
  270. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-72.woff2 +0 -0
  271. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-73.woff2 +0 -0
  272. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-74.woff2 +0 -0
  273. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-75.woff2 +0 -0
  274. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-76.woff2 +0 -0
  275. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-77.woff2 +0 -0
  276. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-78.woff2 +0 -0
  277. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-79.woff2 +0 -0
  278. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-8.woff2 +0 -0
  279. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-80.woff2 +0 -0
  280. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-81.woff2 +0 -0
  281. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-82.woff2 +0 -0
  282. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-83.woff2 +0 -0
  283. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-84.woff2 +0 -0
  284. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-85.woff2 +0 -0
  285. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-86.woff2 +0 -0
  286. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-87.woff2 +0 -0
  287. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-88.woff2 +0 -0
  288. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-89.woff2 +0 -0
  289. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-9.woff2 +0 -0
  290. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-90.woff2 +0 -0
  291. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-91.woff2 +0 -0
  292. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-92.woff2 +0 -0
  293. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-93.woff2 +0 -0
  294. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-94.woff2 +0 -0
  295. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-95.woff2 +0 -0
  296. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-96.woff2 +0 -0
  297. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-97.woff2 +0 -0
  298. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-98.woff2 +0 -0
  299. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-99.woff2 +0 -0
  300. package/web/dist/fonts/source-serif-4/LICENSE +93 -0
  301. package/web/dist/fonts/source-serif-4/source-serif-4-500-0.woff2 +0 -0
  302. package/web/dist/fonts/source-serif-4/source-serif-4-500-1.woff2 +0 -0
  303. package/web/dist/fonts/source-serif-4/source-serif-4-500-2.woff2 +0 -0
  304. package/web/dist/fonts/source-serif-4/source-serif-4-500-3.woff2 +0 -0
  305. package/web/dist/fonts/source-serif-4/source-serif-4-500-4.woff2 +0 -0
  306. package/web/dist/fonts/source-serif-4/source-serif-4-500-5.woff2 +0 -0
  307. package/web/dist/icon/1a-mark.svg +1 -0
  308. package/web/dist/icon.ico +0 -0
  309. package/web/dist/index.html +18 -0
  310. package/web/public/apple-touch-icon.png +0 -0
  311. package/web/public/brand-mark.svg +5 -0
  312. package/web/public/favicon-32.png +0 -0
  313. package/web/public/favicon.svg +5 -0
  314. package/web/public/fonts/WIKI-FONTS.md +5 -0
  315. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  316. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  317. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  318. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
  319. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
  320. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-600-normal.woff2 +0 -0
  321. package/web/public/fonts/ibm-plex-sans/LICENSE +93 -0
  322. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  323. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  324. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  325. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
  326. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
  327. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
  328. package/web/public/fonts/inter/inter-latin-400-normal.woff2 +0 -0
  329. package/web/public/fonts/inter/inter-latin-500-normal.woff2 +0 -0
  330. package/web/public/fonts/inter/inter-latin-600-normal.woff2 +0 -0
  331. package/web/public/fonts/inter/inter-latin-700-normal.woff2 +0 -0
  332. package/web/public/fonts/inter/inter-latin-800-normal.woff2 +0 -0
  333. package/web/public/fonts/inter/inter-latin-ext-400-normal.woff2 +0 -0
  334. package/web/public/fonts/inter/inter-latin-ext-500-normal.woff2 +0 -0
  335. package/web/public/fonts/inter/inter-latin-ext-600-normal.woff2 +0 -0
  336. package/web/public/fonts/inter/inter-latin-ext-700-normal.woff2 +0 -0
  337. package/web/public/fonts/inter/inter-latin-ext-800-normal.woff2 +0 -0
  338. package/web/public/fonts/jetbrains-mono/LICENSE +93 -0
  339. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-400-normal.woff2 +0 -0
  340. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-500-normal.woff2 +0 -0
  341. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-600-normal.woff2 +0 -0
  342. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
  343. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
  344. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-600-normal.woff2 +0 -0
  345. package/web/public/fonts/noto-serif-sc/LICENSE +94 -0
  346. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-0.woff2 +0 -0
  347. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-1.woff2 +0 -0
  348. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-10.woff2 +0 -0
  349. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-100.woff2 +0 -0
  350. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-11.woff2 +0 -0
  351. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-12.woff2 +0 -0
  352. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-13.woff2 +0 -0
  353. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-14.woff2 +0 -0
  354. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-15.woff2 +0 -0
  355. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-16.woff2 +0 -0
  356. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-17.woff2 +0 -0
  357. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-18.woff2 +0 -0
  358. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-19.woff2 +0 -0
  359. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-2.woff2 +0 -0
  360. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-20.woff2 +0 -0
  361. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-21.woff2 +0 -0
  362. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-22.woff2 +0 -0
  363. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-23.woff2 +0 -0
  364. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-24.woff2 +0 -0
  365. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-25.woff2 +0 -0
  366. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-26.woff2 +0 -0
  367. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-27.woff2 +0 -0
  368. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-28.woff2 +0 -0
  369. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-29.woff2 +0 -0
  370. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-3.woff2 +0 -0
  371. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-30.woff2 +0 -0
  372. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-31.woff2 +0 -0
  373. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-32.woff2 +0 -0
  374. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-33.woff2 +0 -0
  375. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-34.woff2 +0 -0
  376. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-35.woff2 +0 -0
  377. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-36.woff2 +0 -0
  378. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-37.woff2 +0 -0
  379. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-38.woff2 +0 -0
  380. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-39.woff2 +0 -0
  381. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-4.woff2 +0 -0
  382. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-40.woff2 +0 -0
  383. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-41.woff2 +0 -0
  384. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-42.woff2 +0 -0
  385. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-43.woff2 +0 -0
  386. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-44.woff2 +0 -0
  387. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-45.woff2 +0 -0
  388. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-46.woff2 +0 -0
  389. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-47.woff2 +0 -0
  390. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-48.woff2 +0 -0
  391. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-49.woff2 +0 -0
  392. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-5.woff2 +0 -0
  393. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-50.woff2 +0 -0
  394. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-51.woff2 +0 -0
  395. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-52.woff2 +0 -0
  396. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-53.woff2 +0 -0
  397. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-54.woff2 +0 -0
  398. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-55.woff2 +0 -0
  399. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-56.woff2 +0 -0
  400. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-57.woff2 +0 -0
  401. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-58.woff2 +0 -0
  402. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-59.woff2 +0 -0
  403. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-6.woff2 +0 -0
  404. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-60.woff2 +0 -0
  405. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-61.woff2 +0 -0
  406. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-62.woff2 +0 -0
  407. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-63.woff2 +0 -0
  408. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-64.woff2 +0 -0
  409. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-65.woff2 +0 -0
  410. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-66.woff2 +0 -0
  411. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-67.woff2 +0 -0
  412. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-68.woff2 +0 -0
  413. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-69.woff2 +0 -0
  414. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-7.woff2 +0 -0
  415. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-70.woff2 +0 -0
  416. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-71.woff2 +0 -0
  417. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-72.woff2 +0 -0
  418. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-73.woff2 +0 -0
  419. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-74.woff2 +0 -0
  420. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-75.woff2 +0 -0
  421. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-76.woff2 +0 -0
  422. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-77.woff2 +0 -0
  423. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-78.woff2 +0 -0
  424. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-79.woff2 +0 -0
  425. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-8.woff2 +0 -0
  426. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-80.woff2 +0 -0
  427. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-81.woff2 +0 -0
  428. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-82.woff2 +0 -0
  429. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-83.woff2 +0 -0
  430. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-84.woff2 +0 -0
  431. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-85.woff2 +0 -0
  432. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-86.woff2 +0 -0
  433. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-87.woff2 +0 -0
  434. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-88.woff2 +0 -0
  435. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-89.woff2 +0 -0
  436. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-9.woff2 +0 -0
  437. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-90.woff2 +0 -0
  438. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-91.woff2 +0 -0
  439. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-92.woff2 +0 -0
  440. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-93.woff2 +0 -0
  441. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-94.woff2 +0 -0
  442. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-95.woff2 +0 -0
  443. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-96.woff2 +0 -0
  444. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-97.woff2 +0 -0
  445. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-98.woff2 +0 -0
  446. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-99.woff2 +0 -0
  447. package/web/public/fonts/source-serif-4/LICENSE +93 -0
  448. package/web/public/fonts/source-serif-4/source-serif-4-500-0.woff2 +0 -0
  449. package/web/public/fonts/source-serif-4/source-serif-4-500-1.woff2 +0 -0
  450. package/web/public/fonts/source-serif-4/source-serif-4-500-2.woff2 +0 -0
  451. package/web/public/fonts/source-serif-4/source-serif-4-500-3.woff2 +0 -0
  452. package/web/public/fonts/source-serif-4/source-serif-4-500-4.woff2 +0 -0
  453. package/web/public/fonts/source-serif-4/source-serif-4-500-5.woff2 +0 -0
  454. package/web/public/icon/1a-mark.svg +1 -0
  455. package/web/public/icon.ico +0 -0
@@ -0,0 +1,154 @@
1
+ import { killPidTree, lookupProcessName, lookupProcessCommandLine, snapshotListeningPorts, } from "./process-utils.js";
2
+ const BG_REFRESH_INTERVAL_MS = 30_000;
3
+ /** bash 结束后等这么久再拍「后」快照——给后台服务绑定端口的时间。 */
4
+ const BG_BIND_WAIT_MS = 1500;
5
+ export class BgServerTracker {
6
+ opts;
7
+ servers = new Map();
8
+ /** bash 工具开始执行前拍的监听端口快照(tool_execution_start 时设置)。 */
9
+ listenBefore = null;
10
+ refreshTimer = null;
11
+ constructor(opts) {
12
+ this.opts = opts;
13
+ }
14
+ /** True while AI-started background servers are tracked (keeps the client session alive). */
15
+ hasServers() {
16
+ return this.servers.size > 0;
17
+ }
18
+ /** 启动周期性存活检查(死项静默剔除)。 */
19
+ start() {
20
+ this.refreshTimer = setInterval(() => void this.refresh(), BG_REFRESH_INTERVAL_MS);
21
+ this.refreshTimer.unref?.();
22
+ }
23
+ stop() {
24
+ if (this.refreshTimer) {
25
+ clearInterval(this.refreshTimer);
26
+ this.refreshTimer = null;
27
+ }
28
+ }
29
+ /** tool_execution_start(bash):先记下「前」快照。 */
30
+ snapshotBefore() {
31
+ void snapshotListeningPorts().then((m) => {
32
+ this.listenBefore = m;
33
+ });
34
+ }
35
+ /** After a bash tool run, wait briefly for background servers to bind,
36
+ * then diff the listening-port snapshot against the pre-run one and
37
+ * remember anything new — those are servers the agent left running. */
38
+ async trackAfterBash() {
39
+ const before = this.listenBefore;
40
+ this.listenBefore = null;
41
+ if (!before)
42
+ return;
43
+ await new Promise((r) => setTimeout(r, BG_BIND_WAIT_MS));
44
+ const after = await snapshotListeningPorts();
45
+ let added = false;
46
+ for (const [port, pid] of after) {
47
+ if (!before.has(port) && !this.servers.has(port)) {
48
+ this.servers.set(port, { pid, since: Date.now() });
49
+ added = true;
50
+ // Best-effort process name + full command line so the panel shows
51
+ // something readable (name) AND what is actually running (command).
52
+ void lookupProcessName(pid).then((name) => {
53
+ const cur = this.servers.get(port);
54
+ if (cur && cur.pid === pid && name) {
55
+ cur.name = name;
56
+ this.push();
57
+ }
58
+ });
59
+ void lookupProcessCommandLine(pid).then((command) => {
60
+ const cur = this.servers.get(port);
61
+ if (cur && cur.pid === pid && command) {
62
+ cur.command = command;
63
+ this.push();
64
+ }
65
+ });
66
+ this.opts.emit({
67
+ type: "notice",
68
+ level: "info",
69
+ text: `检测到 AI 启动的后台服务:端口 ${port}(pid ${pid})——可在顶栏「后台任务」里单独停止或全部关闭`,
70
+ });
71
+ }
72
+ }
73
+ if (added)
74
+ this.push();
75
+ }
76
+ /** The current background-server list, oldest first. 合并插件任务。 */
77
+ list() {
78
+ const out = [...this.servers.entries()]
79
+ .map(([port, v]) => ({
80
+ port,
81
+ pid: v.pid,
82
+ since: v.since,
83
+ ...(v.name ? { name: v.name } : {}),
84
+ ...(v.command ? { command: v.command } : {}),
85
+ }))
86
+ .sort((a, b) => a.since - b.since);
87
+ for (const t of this.opts.pluginTasks?.() ?? [])
88
+ out.push(t);
89
+ return out;
90
+ }
91
+ /** Push the current background-task list to every connected socket. */
92
+ push() {
93
+ this.opts.emit({ type: "bg_servers", servers: this.list() });
94
+ }
95
+ /** Re-snapshot listening ports and drop tracked entries that are no longer
96
+ * listening — the process exited on its own, so it must leave the panel.
97
+ * Port AND pid must both match: a port reused by an unrelated process is
98
+ * not our server anymore. Silent (the list just updates). */
99
+ async refresh() {
100
+ if (this.opts.isDisposed() || this.servers.size === 0)
101
+ return;
102
+ const now = await snapshotListeningPorts();
103
+ let changed = false;
104
+ for (const [port, v] of [...this.servers]) {
105
+ if (now.get(port) !== v.pid) {
106
+ this.servers.delete(port);
107
+ changed = true;
108
+ }
109
+ }
110
+ if (changed)
111
+ this.push();
112
+ }
113
+ /** Re-push the current list on request (panel opened); prunes dead entries first. */
114
+ async listAndPush() {
115
+ await this.refresh();
116
+ this.push();
117
+ }
118
+ /** Kill ONE background server (by port); returns whether anything was killed. */
119
+ async killOne(port) {
120
+ const entry = this.servers.get(port);
121
+ if (!entry) {
122
+ this.opts.emit({
123
+ type: "notice",
124
+ level: "info",
125
+ text: `端口 ${port} 不在后台任务列表中`,
126
+ });
127
+ this.opts.flushSnapshot();
128
+ return false;
129
+ }
130
+ killPidTree(entry.pid);
131
+ this.servers.delete(port);
132
+ this.push();
133
+ this.opts.emit({
134
+ type: "notice",
135
+ level: "info",
136
+ text: `已停止后台任务:端口 ${port}(pid ${entry.pid})`,
137
+ });
138
+ this.opts.flushSnapshot();
139
+ return true;
140
+ }
141
+ /** Kill every background server the agent started; returns the freed ports. */
142
+ async killAll() {
143
+ if (this.servers.size === 0)
144
+ return [];
145
+ const killed = [];
146
+ for (const [port, { pid }] of [...this.servers]) {
147
+ killPidTree(pid);
148
+ killed.push(String(port));
149
+ }
150
+ this.servers.clear();
151
+ this.push();
152
+ return killed;
153
+ }
154
+ }
@@ -0,0 +1,19 @@
1
+ export const DEFAULT_CLIENT_IDLE_MS = 30 * 60_000;
2
+ export function clientIdleMsFromEnv(value) {
3
+ if (value === undefined || value.trim() === "")
4
+ return DEFAULT_CLIENT_IDLE_MS;
5
+ const minutes = Number(value);
6
+ return Number.isFinite(minutes) && minutes >= 0 ? minutes * 60_000 : DEFAULT_CLIENT_IDLE_MS;
7
+ }
8
+ /** idleMs <= 0 disables eviction. */
9
+ export function isClientEvictable(facts, now, idleMs) {
10
+ if (idleMs <= 0 || facts.disposed || facts.switchingWorkspace)
11
+ return false;
12
+ if (facts.socketCount > 0 || facts.idleSince <= 0)
13
+ return false;
14
+ if (facts.streamingConversations > 0 || facts.queuedMessages > 0)
15
+ return false;
16
+ if (facts.liveTerminals > 0 || facts.backgroundTasks > 0)
17
+ return false;
18
+ return now - facts.idleSince >= idleMs;
19
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * clientId arrives from the browser and is later used in file paths (uploads/<clientId>/)
3
+ * and as a map key. Real ids are UUIDs; accept any plain token that cannot
4
+ * contain a path separator or start with a dot.
5
+ */
6
+ const CLIENT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
7
+ export function isValidClientId(value) {
8
+ return typeof value === "string" && CLIENT_ID_RE.test(value);
9
+ }
@@ -0,0 +1,202 @@
1
+ /** Per-client UI preferences and project/session navigation, persisted best-effort. */
2
+ import { readFileSync } from "node:fs";
3
+ import { writeFileAtomic } from "./private-file.js";
4
+ import { dirname } from "node:path";
5
+ /** Stable identity of an extension for its read-only resource view: the npm
6
+ * spec for packages (survives version bumps), the resolved entry path
7
+ * otherwise. */
8
+ export function extensionKey(e) {
9
+ const src = e.sourceInfo;
10
+ if (src?.origin === "package" && src.source)
11
+ return src.source;
12
+ return src?.path ?? e.path;
13
+ }
14
+ /** Upper bound on persisted client entries (see ClientStateStore.prune). */
15
+ const MAX_CLIENT_ENTRIES = 200;
16
+ /**
17
+ * Merge persisted projects with disk-discovered ones into the project list:
18
+ * ordered by latest use/activity, newest first. Persisted first-added values
19
+ * remain as stable tie-breakers and for legacy data migration.
20
+ * Tombstoned entries (explicitly removed by the user) stay hidden; the list is
21
+ * capped at 20, dropping the least recently used first.
22
+ */
23
+ export function mergeProjectSummaries(saved, disk, removed) {
24
+ const firstAdded = new Map();
25
+ const lastUsed = new Map();
26
+ for (const p of saved) {
27
+ firstAdded.set(p.path, p.firstAdded ?? p.lastUsed);
28
+ lastUsed.set(p.path, p.lastUsed);
29
+ }
30
+ for (const p of disk) {
31
+ if (!firstAdded.has(p.path))
32
+ firstAdded.set(p.path, p.firstAdded);
33
+ lastUsed.set(p.path, Math.max(lastUsed.get(p.path) ?? 0, p.lastUsed));
34
+ }
35
+ const byPath = new Map(disk.map((p) => [p.path, p]));
36
+ return [...firstAdded.entries()]
37
+ .filter(([path]) => !removed.has(path))
38
+ .map(([path, added]) => ({
39
+ path,
40
+ firstAdded: added,
41
+ lastUsed: lastUsed.get(path) ?? 0,
42
+ lastConversationAt: byPath.get(path)?.lastUsed,
43
+ conversationCount: byPath.get(path)?.conversationCount ?? 0,
44
+ }))
45
+ .sort((a, b) => b.lastUsed - a.lastUsed || b.firstAdded - a.firstAdded)
46
+ .slice(0, 20);
47
+ }
48
+ /**
49
+ * Persists which workspace each browser client last used + which workspaces it
50
+ * has opened, so a server restart / page reload restores the same project and
51
+ * the UI can offer a one-click recent-project list. File I/O is best-effort:
52
+ * persistence problems must never crash the server or block a session.
53
+ */
54
+ export class ClientStateStore {
55
+ filePath;
56
+ cache = null;
57
+ constructor(filePath) {
58
+ this.filePath = filePath;
59
+ }
60
+ load() {
61
+ if (this.cache)
62
+ return this.cache;
63
+ try {
64
+ const parsed = JSON.parse(readFileSync(this.filePath, "utf8"));
65
+ this.cache = parsed && typeof parsed === "object" ? parsed : {};
66
+ }
67
+ catch {
68
+ this.cache = {};
69
+ }
70
+ return this.cache;
71
+ }
72
+ /** Get or create a client's entry and stamp it as recently used. */
73
+ entry(clientId) {
74
+ const all = this.load();
75
+ const state = (all[clientId] ??= { projects: [] });
76
+ state.seen = Date.now();
77
+ return state;
78
+ }
79
+ /** Browser tabs are a new clientId each; keep the file bounded by dropping the
80
+ * least recently used entries (the desktop app's stable id stays near the top). */
81
+ prune() {
82
+ const all = this.cache;
83
+ if (!all)
84
+ return;
85
+ const ids = Object.keys(all);
86
+ if (ids.length <= MAX_CLIENT_ENTRIES)
87
+ return;
88
+ const lastSeen = (state) => state.seen ?? Math.max(0, ...(state.projects ?? []).map((p) => p.lastUsed));
89
+ ids.sort((a, b) => lastSeen(all[b]) - lastSeen(all[a]));
90
+ for (const id of ids.slice(MAX_CLIENT_ENTRIES))
91
+ delete all[id];
92
+ }
93
+ save() {
94
+ this.prune();
95
+ try {
96
+ // Atomic write (tmp + rename): a crash mid-write must never leave a
97
+ // half-written JSON — that would wipe ALL persisted state (recent
98
+ // projects and display settings) on next load. 0600: it lists the
99
+ // user's project paths.
100
+ writeFileAtomic(this.filePath, JSON.stringify(this.cache, null, 2) + "\n");
101
+ }
102
+ catch {
103
+ // best effort
104
+ }
105
+ }
106
+ get(clientId) {
107
+ return this.load()[clientId] ?? { projects: [] };
108
+ }
109
+ /** Freeze the order anchors of projects discovered from old session files as
110
+ * soon as they are shown. Selecting one later must not make it "new". */
111
+ rememberDisplayedProjects(clientId, projects) {
112
+ const state = this.entry(clientId);
113
+ const known = new Set(state.projects.map((p) => p.path));
114
+ let changed = false;
115
+ for (const project of projects) {
116
+ if (known.has(project.path))
117
+ continue;
118
+ state.projects.push({ path: project.path, lastUsed: project.lastUsed, firstAdded: project.firstAdded });
119
+ known.add(project.path);
120
+ changed = true;
121
+ }
122
+ if (changed) {
123
+ state.projects.sort((a, b) => b.lastUsed - a.lastUsed || (b.firstAdded ?? 0) - (a.firstAdded ?? 0));
124
+ state.projects = state.projects.slice(0, 30);
125
+ this.save();
126
+ }
127
+ }
128
+ /** Remember which workspace a client last used and move it to the front. */
129
+ remember(clientId, cwd) {
130
+ const state = this.entry(clientId);
131
+ state.lastCwd = cwd;
132
+ // Distinct clicks can share one millisecond; keep ordering deterministic.
133
+ const now = Math.max(Date.now(), ...state.projects.map((project) => project.lastUsed + 1));
134
+ const existing = state.projects.find((p) => p.path === cwd);
135
+ if (existing) {
136
+ // Keep the historical first-added value as a deterministic tie-breaker.
137
+ existing.firstAdded ??= existing.lastUsed;
138
+ existing.lastUsed = now;
139
+ }
140
+ else {
141
+ state.projects.push({ path: cwd, lastUsed: now, firstAdded: now });
142
+ }
143
+ state.projects.sort((a, b) => b.lastUsed - a.lastUsed || (b.firstAdded ?? 0) - (a.firstAdded ?? 0));
144
+ state.projects = state.projects.slice(0, 30);
145
+ // Opening the workspace again clears its removal tombstone.
146
+ if (state.removedProjects?.length) {
147
+ state.removedProjects = state.removedProjects.filter((p) => p !== cwd);
148
+ }
149
+ this.save();
150
+ }
151
+ /** Drop one workspace from the recent-project list (user-requested removal).
152
+ * Records a tombstone too: pushProjects() re-discovers cwds from session
153
+ * files on every listing, so without it the entry would instantly reappear. */
154
+ removeProject(clientId, cwd) {
155
+ const state = this.entry(clientId);
156
+ state.projects = state.projects.filter((p) => p.path !== cwd);
157
+ if (state.lastCwd === cwd)
158
+ delete state.lastCwd;
159
+ const removed = new Set(state.removedProjects ?? []);
160
+ removed.add(cwd);
161
+ state.removedProjects = [...removed];
162
+ this.save();
163
+ }
164
+ /** Tombstoned projects (explicitly removed by the user) for filtering the
165
+ * merged recent-project list. */
166
+ getRemovedProjects(clientId) {
167
+ return this.load()[clientId]?.removedProjects ?? [];
168
+ }
169
+ /** Remember conversations that were still streaming at shutdown (best-
170
+ * effort; called during the graceful-shutdown path). */
171
+ saveInterrupted(clientId, list) {
172
+ if (list.length === 0)
173
+ return;
174
+ const state = this.entry(clientId);
175
+ state.interrupted = list.slice(0, 8);
176
+ this.save();
177
+ }
178
+ /** Consume the interrupted-conversation record (returns and clears it) —
179
+ * called once on the client's first attach after a restart. */
180
+ takeInterrupted(clientId) {
181
+ const all = this.load();
182
+ const state = all[clientId];
183
+ const list = state?.interrupted;
184
+ if (list?.length && state) {
185
+ delete state.interrupted;
186
+ this.save();
187
+ }
188
+ return list;
189
+ }
190
+ /** Last-used settings-panel state for a client, or defaults. */
191
+ getSettings(clientId) {
192
+ const settings = this.load()[clientId]?.settings;
193
+ return { editResendNewSession: settings?.editResendNewSession ?? false, thinkingWrap: settings?.thinkingWrap ?? false, toolsWrap: settings?.toolsWrap ?? true, disabledPlugins: settings?.disabledPlugins ?? [] };
194
+ }
195
+ /** Persist the client's settings-panel state (partial merge). */
196
+ saveSettings(clientId, partial) {
197
+ const state = this.entry(clientId);
198
+ const current = this.getSettings(clientId);
199
+ state.settings = { editResendNewSession: partial.editResendNewSession ?? current.editResendNewSession ?? false, thinkingWrap: partial.thinkingWrap ?? current.thinkingWrap, toolsWrap: partial.toolsWrap ?? current.toolsWrap, disabledPlugins: partial.disabledPlugins ?? current.disabledPlugins };
200
+ this.save();
201
+ }
202
+ }
@@ -0,0 +1,36 @@
1
+ import { realpathSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import { sniffImageMime } from "./text-sniff.js";
5
+ /** A user save, separate from agent tools/context; only new files in the active workspace. */
6
+ export function installCodemodeImageRoutes(app, service, originAllowed) {
7
+ app.post("/api/codemode-image", (req, res) => {
8
+ res.setHeader("Cache-Control", "no-store");
9
+ if (!originAllowed(req) || !req.is("application/json")) {
10
+ res.sendStatus(403);
11
+ return;
12
+ }
13
+ const { clientId, cwd, dataUrl } = req.body ?? {};
14
+ const cs = typeof clientId === "string" ? service().get(clientId) : undefined;
15
+ if (!cs || cs.switchingWorkspace || cs.cwd !== cwd || service().quiesceInfo().quiesced) {
16
+ res.status(409).json({ error: "Workspace unavailable" });
17
+ return;
18
+ }
19
+ try {
20
+ if (typeof dataUrl !== "string" || dataUrl.length > 9 * 1024 * 1024)
21
+ throw new Error("Image exceeds save limit (6 MB)");
22
+ const match = /^data:image\/(png|jpeg|gif|webp);base64,([A-Za-z0-9+/]+={0,2})$/.exec(dataUrl);
23
+ if (!match)
24
+ throw new Error("Invalid image data");
25
+ const buffer = Buffer.from(match[2], "base64");
26
+ if (buffer.length > 6 * 1024 * 1024 || sniffImageMime(buffer, "") !== `image/${match[1]}`)
27
+ throw new Error("Invalid image format or size");
28
+ const path = `codemode-${randomUUID()}.${match[1] === "jpeg" ? "jpg" : match[1]}`;
29
+ writeFileSync(join(realpathSync(cwd), path), buffer, { flag: "wx", mode: 0o600 });
30
+ res.json({ path });
31
+ }
32
+ catch (error) {
33
+ res.status(400).json({ error: error.message });
34
+ }
35
+ });
36
+ }
@@ -0,0 +1,19 @@
1
+ /** Allowlist native renderer details; never forward arbitrary extension metadata. */
2
+ export function codemodeDetails(value) {
3
+ if (!value || typeof value !== "object" || !Array.isArray(value.calls))
4
+ return;
5
+ const raw = value;
6
+ const calls = [];
7
+ for (const item of raw.calls.slice(0, 256)) {
8
+ if (!item || typeof item !== "object")
9
+ continue;
10
+ const call = item;
11
+ if (typeof call.id !== "string" || typeof call.name !== "string" || !["running", "ok", "error", "cancelled"].includes(String(call.status)))
12
+ continue;
13
+ calls.push({ id: call.id.slice(0, 500), name: call.name.slice(0, 300), args: typeof call.args === "string" ? call.args.slice(0, 4000) : "", status: call.status,
14
+ ...(typeof call.durationMs === "number" && Number.isFinite(call.durationMs) && call.durationMs >= 0 ? { durationMs: call.durationMs } : {}),
15
+ ...(typeof call.cost === "number" && Number.isFinite(call.cost) && call.cost >= 0 ? { cost: call.cost } : {}),
16
+ ...(typeof call.error === "string" ? { error: call.error.slice(0, 4000) } : {}), });
17
+ }
18
+ return { calls, totalCalls: raw.calls.length, ...(typeof raw.fullOutputPath === "string" ? { fullOutputPath: raw.fullOutputPath.slice(0, 4096) } : {}) };
19
+ }
@@ -0,0 +1,135 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { DefaultPackageManager, VERSION } from "@earendil-works/pi-coding-agent";
5
+ import { extensionDisplay } from "./extension-display.js";
6
+ let restartRequired = false;
7
+ export const componentRestartRequired = () => restartRequired;
8
+ export function markComponentUpdated() { restartRequired = true; }
9
+ const npmSource = /^npm:((?:@[a-z0-9._-]+\/)?[a-z0-9._-]+)(?:@([^\s]+))?$/i;
10
+ function packageInfo(directory) {
11
+ try {
12
+ const value = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
13
+ return { name: typeof value?.name === "string" ? value.name : undefined, version: typeof value?.version === "string" ? value.version : undefined };
14
+ }
15
+ catch {
16
+ return {};
17
+ }
18
+ }
19
+ export function packageManagerFor(session, cwd, agentDir) {
20
+ return new DefaultPackageManager({ cwd, agentDir, settingsManager: session.settingsManager });
21
+ }
22
+ export function updateTargets(session, manager) {
23
+ const targets = [
24
+ { id: "builtin:agent", name: "pi Agent", current: VERSION, kind: "bundled", packageName: "@earendil-works/pi-coding-agent" },
25
+ ];
26
+ const packages = manager.listConfiguredPackages();
27
+ for (const pkg of packages) {
28
+ const parsed = npmSource.exec(pkg.source);
29
+ const metadata = pkg.installedPath ? packageInfo(pkg.installedPath) : {};
30
+ const git = /^(?:git:|https?:\/\/|git@|ssh:\/\/)/.test(pkg.source);
31
+ targets.push({ id: `${pkg.scope}:${pkg.source}`, name: parsed?.[1] ?? metadata.name ?? pkg.source, source: pkg.source, scope: pkg.scope, directory: pkg.installedPath, kind: parsed && ["@youweichen/pi-harness", "pi-harness"].includes(parsed[1]) ? "bundled" : parsed ? "npm" : git ? "git" : "local", packageName: parsed?.[1], current: metadata.version ?? null, ambiguous: packages.filter((other) => parsed ? npmSource.exec(other.source)?.[1] === parsed[1] : other.source === pkg.source).length > 1, pinned: parsed ? !!parsed[2] : git && pkg.source.includes("#") });
32
+ }
33
+ const roots = packages.flatMap((pkg) => pkg.installedPath ? [pkg.installedPath.replaceAll("\\", "/").replace(/\/$/, "") + "/"] : []);
34
+ for (const extension of session.resourceLoader.getExtensions().extensions) {
35
+ if (extension.hidden || extension.resolvedPath.startsWith("builtin:"))
36
+ continue;
37
+ const path = extension.resolvedPath;
38
+ if (roots.some((root) => path.replaceAll("\\", "/").startsWith(root)))
39
+ continue;
40
+ targets.push({ id: `local:${path}`, name: extensionDisplay(path).name, current: null, kind: "local" });
41
+ }
42
+ return targets;
43
+ }
44
+ const parseVersion = (value) => /^(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
45
+ /** Only stable registry releases are offered; prerelease builds are not silently selected. */
46
+ export function hasNewerStableVersion(current, latest) {
47
+ const a = parseVersion(current), b = parseVersion(latest);
48
+ if (!a || !b || b[4])
49
+ return false;
50
+ for (let i = 1; i <= 3; i++)
51
+ if (Number(a[i]) !== Number(b[i]))
52
+ return Number(b[i]) > Number(a[i]);
53
+ return !!a[4];
54
+ }
55
+ const registryCache = new Map();
56
+ export async function latestNpmVersion(name) {
57
+ const cached = registryCache.get(name);
58
+ if (cached && cached.until > Date.now())
59
+ return cached.promise;
60
+ const promise = (async () => {
61
+ const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}/latest`, { signal: AbortSignal.timeout(8000) });
62
+ if (!response.ok)
63
+ throw Error(`HTTP ${response.status}`);
64
+ const data = await response.json();
65
+ if (typeof data.version !== "string" || !/^\d+\.\d+\.\d+(?:[-+][\w.-]+)?$/.test(data.version))
66
+ throw Error("Invalid registry version");
67
+ return data.version;
68
+ })();
69
+ registryCache.set(name, { until: Date.now() + 300_000, promise });
70
+ if (registryCache.size > 256)
71
+ registryCache.delete(registryCache.keys().next().value);
72
+ try {
73
+ return await promise;
74
+ }
75
+ catch (error) {
76
+ registryCache.delete(name);
77
+ throw error;
78
+ }
79
+ }
80
+ export const gitOutput = (cwd, args) => new Promise((resolve, reject) => {
81
+ execFile("git", args, { cwd, timeout: 8000, maxBuffer: 1024 * 1024, windowsHide: true, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } }, (error, stdout) => error ? reject(Error("Git update check failed")) : resolve(stdout.trim()));
82
+ });
83
+ export async function checkComponent(target, npm = latestNpmVersion, git = gitOutput) {
84
+ const result = { id: target.id, name: target.name, current: target.current, latest: null, kind: target.kind, scope: target.scope, status: "manual", canUpdate: false };
85
+ try {
86
+ if (target.kind === "local")
87
+ return result;
88
+ if (target.kind === "git") {
89
+ if (!target.directory)
90
+ throw Error("Package not installed");
91
+ result.current = (await git(target.directory, ["rev-parse", "HEAD"])).slice(0, 12);
92
+ if (target.pinned)
93
+ return { ...result, status: "pinned" };
94
+ const upstream = await git(target.directory, ["rev-parse", "--abbrev-ref", "@{upstream}"]).catch(() => "");
95
+ const ref = upstream.startsWith("origin/") ? `refs/heads/${upstream.slice(7)}` : "HEAD";
96
+ const remote = await git(target.directory, ["ls-remote", "origin", ref]);
97
+ const sha = /^([0-9a-f]{40,64})\s/m.exec(remote)?.[1];
98
+ if (!sha)
99
+ throw Error("Remote version unavailable");
100
+ result.latest = sha.slice(0, 12);
101
+ }
102
+ else {
103
+ if (!target.packageName)
104
+ throw Error("Package name unavailable");
105
+ result.latest = await npm(target.packageName);
106
+ }
107
+ const comparable = !!result.current && (target.kind === "git" || !!parseVersion(result.current) && !!parseVersion(result.latest) && !parseVersion(result.latest)[4]);
108
+ const available = comparable && (target.kind === "git" ? result.current !== result.latest : hasNewerStableVersion(result.current, result.latest));
109
+ return { ...result, status: target.ambiguous ? "manual" : target.pinned ? "pinned" : !comparable ? "unknown" : available ? "available" : "current", canUpdate: available && !target.pinned && !target.ambiguous && target.kind !== "bundled" };
110
+ }
111
+ catch (error) {
112
+ return { ...result, status: "error", error: error instanceof Error ? error.message : "Check failed" };
113
+ }
114
+ }
115
+ export async function checkComponents(targets) {
116
+ const results = [];
117
+ // Keep registry/git concurrency bounded for installations with many extensions.
118
+ for (let i = 0; i < targets.length; i += 4)
119
+ results.push(...await Promise.all(targets.slice(i, i + 4).map((target) => checkComponent(target))));
120
+ return results;
121
+ }
122
+ /** Resolve updates from the server catalog, never a client-supplied package spec. */
123
+ export async function updateComponentPackage(targets, id, update, check = checkComponent, git = gitOutput) {
124
+ const target = targets.find((entry) => entry.id === id);
125
+ if (!target?.source || target.kind === "bundled" || target.kind === "local" || target.pinned)
126
+ throw Error("该组件需要随应用升级或手动维护");
127
+ if (target.ambiguous)
128
+ throw Error("同一来源安装在多个范围,请按范围手动更新");
129
+ if (!(await check(target)).canUpdate)
130
+ throw Error("没有可安装的更新,请重新检查");
131
+ if (target.kind === "git" && target.directory && await git(target.directory, ["status", "--porcelain"]))
132
+ throw Error("扩展目录有本地改动,请先保存或提交后再更新");
133
+ await update(target.source);
134
+ markComponentUpdated();
135
+ }
@@ -0,0 +1,56 @@
1
+ /** Provider APIs report one context total, not a breakdown by content origin.
2
+ * Allocate that total using the SDK's chars/4 heuristic (images ≈1200 tokens).
3
+ * The UI must label these four values as estimates. */
4
+ export function estimateContextParts(messages, systemPrompt, totalTokens) {
5
+ if (totalTokens === null || !Number.isFinite(totalTokens) || totalTokens < 0)
6
+ return null;
7
+ const raw = { system: systemPrompt.length / 4, tools: 0, conversation: 0, attachments: 0 };
8
+ const content = (value, category) => {
9
+ if (typeof value === "string") {
10
+ raw[category] += value.length / 4;
11
+ return;
12
+ }
13
+ if (!Array.isArray(value))
14
+ return;
15
+ for (const block of value) {
16
+ if (!block || typeof block !== "object")
17
+ continue;
18
+ if (block.type === "image") {
19
+ raw.attachments += 1200;
20
+ continue;
21
+ }
22
+ if (block.type === "toolCall") {
23
+ raw.tools += (String(block.name ?? "").length + JSON.stringify(block.arguments ?? {}).length) / 4;
24
+ continue;
25
+ }
26
+ if (typeof block.text === "string")
27
+ raw[category] += block.text.length / 4;
28
+ if (typeof block.thinking === "string")
29
+ raw[category] += block.thinking.length / 4;
30
+ }
31
+ };
32
+ for (const message of messages) {
33
+ if (message.role === "assistant" || message.role === "user")
34
+ content(message.content, "conversation");
35
+ else if (message.role === "toolResult" || message.role === "custom")
36
+ content(message.content, "tools");
37
+ else if (message.role === "bashExecution")
38
+ raw.tools += (message.command.length + message.output.length) / 4;
39
+ else if (message.role === "branchSummary" || message.role === "compactionSummary")
40
+ raw.conversation += message.summary.length / 4;
41
+ }
42
+ const keys = ["system", "tools", "conversation", "attachments"];
43
+ const rawTotal = keys.reduce((sum, key) => sum + raw[key], 0);
44
+ if (rawTotal <= 0)
45
+ return { system: 0, tools: 0, conversation: Math.round(totalTokens), attachments: 0 };
46
+ const target = Math.round(totalTokens);
47
+ const exact = keys.map((key) => raw[key] * target / rawTotal);
48
+ const values = exact.map(Math.floor);
49
+ let remaining = target - values.reduce((sum, value) => sum + value, 0);
50
+ for (const index of keys.map((_, index) => index).sort((a, b) => exact[b] % 1 - exact[a] % 1)) {
51
+ if (remaining-- <= 0)
52
+ break;
53
+ values[index]++;
54
+ }
55
+ return { system: values[0], tools: values[1], conversation: values[2], attachments: values[3] };
56
+ }