@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,833 @@
1
+ /**
2
+ * TerminalManager — conversation-owned PTY sessions (node-pty) bridged over
3
+ * the WebSocket protocol, plus the user command list persisted in
4
+ * `<workspaceRoot>/.pi/commands.json`.
5
+ *
6
+ * Each conversation gets its own manager; terminals are shared across browser
7
+ * tabs through the session emit. A socket drop does not kill them: the
8
+ * conversation owns their lifecycle and releases them on disposal.
9
+ *
10
+ * Commands file format:
11
+ * { "commands": [ { "name": "dev", "command": "npm run dev", "cwd": "${pwd}" } ] }
12
+ * `${pwd}` inside cwd/command resolves to the agent session's current working
13
+ * directory (the same directory the agent operates in — see set_cwd).
14
+ */
15
+ import { chmodSync, existsSync, readdirSync, realpathSync, statSync } from "node:fs";
16
+ import { randomUUID } from "node:crypto";
17
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
18
+ import { createRequire } from "node:module";
19
+ import { homedir, tmpdir } from "node:os";
20
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
21
+ // MUST run before node-pty is required: rewrites the installed node-pty copies
22
+ // so their worker/agent handlers tolerate Node `--watch`'s IPC traffic (see the
23
+ // module itself for details).
24
+ import "./patch-node-pty.js";
25
+ import { spawn } from "node-pty";
26
+ /** Location of the command list for a project: <workspaceRoot>/.pi/commands.json */
27
+ export function commandsFilePath(workspaceRoot) {
28
+ return join(workspaceRoot, ".pi", "commands.json");
29
+ }
30
+ /** Expand ${pwd} (and ~) in a cwd/command string against the session's cwd. */
31
+ export function expandPwd(input, pwd) {
32
+ let out = input.replace(/\$\{pwd\}/g, pwd);
33
+ if (out === "~")
34
+ return homedir();
35
+ if (out.startsWith("~/"))
36
+ out = join(homedir(), out.slice(2));
37
+ return out;
38
+ }
39
+ /** Resolve a command's directory: default to the session cwd, expand ${pwd}/~, resolve relative paths. */
40
+ export function resolveCommandCwd(cwd, pwd) {
41
+ if (!cwd || cwd.trim() === "")
42
+ return pwd;
43
+ const expanded = expandPwd(cwd.trim(), pwd);
44
+ return isAbsolute(expanded) ? expanded : resolve(pwd, expanded);
45
+ }
46
+ /** Read the command list; missing file → empty list; malformed → empty list + warning text. */
47
+ export async function loadCommands(workspaceRoot) {
48
+ const path = commandsFilePath(workspaceRoot);
49
+ const { commands, warning } = await readCommandsFile(path);
50
+ return { commands, path, warning };
51
+ }
52
+ async function readCommandsFile(path) {
53
+ if (!existsSync(path))
54
+ return { commands: [] };
55
+ let raw;
56
+ try {
57
+ raw = await readFile(path, "utf8");
58
+ }
59
+ catch (err) {
60
+ return {
61
+ commands: [],
62
+ warning: `读取命令文件失败:${err.message}`,
63
+ };
64
+ }
65
+ let parsed;
66
+ try {
67
+ parsed = JSON.parse(raw);
68
+ }
69
+ catch {
70
+ return { commands: [], warning: `命令文件不是有效 JSON:${path}` };
71
+ }
72
+ if (Array.isArray(parsed)) {
73
+ // Tolerate a bare array: [{name, command, cwd}]
74
+ return {
75
+ commands: parsed
76
+ .filter((c) => typeof c === "object" &&
77
+ c !== null &&
78
+ typeof c.name === "string" &&
79
+ typeof c.command === "string")
80
+ .map((c) => ({ name: c.name, command: c.command, cwd: c.cwd })),
81
+ };
82
+ }
83
+ const obj = parsed;
84
+ if (obj && Array.isArray(obj.commands)) {
85
+ return {
86
+ commands: obj.commands
87
+ .filter((c) => typeof c === "object" &&
88
+ c !== null &&
89
+ typeof c.name === "string" &&
90
+ typeof c.command === "string")
91
+ .map((c) => ({ name: c.name, command: c.command, cwd: c.cwd })),
92
+ };
93
+ }
94
+ return { commands: [], warning: `命令文件格式不正确:${path}` };
95
+ }
96
+ /** Persist the command list, creating .pi/ if needed. */
97
+ export async function saveCommandsFile(workspaceRoot, commands) {
98
+ const path = commandsFilePath(workspaceRoot);
99
+ try {
100
+ await mkdir(join(workspaceRoot, ".pi"), { recursive: true });
101
+ const payload = { commands };
102
+ await writeFile(path, JSON.stringify(payload, null, 2) + "\n", "utf8");
103
+ return { path };
104
+ }
105
+ catch (err) {
106
+ return { path, error: `保存命令文件失败:${err.message}` };
107
+ }
108
+ }
109
+ const isWindows = process.platform === "win32";
110
+ const MAX_TERMINALS = 16;
111
+ /** Coalescing window for terminal_output WS messages (one animation frame). */
112
+ const OUTPUT_FLUSH_MS = 16;
113
+ const MAX_TERMINAL_HISTORY = 32;
114
+ const MAX_OUTPUT = 200_000;
115
+ const MAX_INPUT = 64 * 1024;
116
+ const MAX_ID = 80;
117
+ // ---------------------------------------------------------------------------
118
+ // 终端接管 bash(terminal-backed bash tool)
119
+ // ---------------------------------------------------------------------------
120
+ /** 去掉 ANSI 转义序列(OSC/CSI/其余 ESC 序列)与孤立 CR(进度条重绘),
121
+ * 让 PTY 回显变成 bash 工具风格的纯文本。 */
122
+ export function stripAnsi(s) {
123
+ return s
124
+ .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "") // OSC(标题/超链接等)
125
+ .replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "") // CSI(颜色/光标/清屏等)
126
+ .replace(/\x1b[@-_]/g, "") // 其余单字符 ESC 序列
127
+ .replace(/\r(?!\n)/g, ""); // 孤立 CR(进度条原地重绘)
128
+ }
129
+ /** 截断过长的工具结果:保留头尾,中间省略。 */
130
+ /** `-i` makes bash interactive; cmd.exe / powershell.exe are interactive on their own. */
131
+ function bashArgs(shell) {
132
+ return /[\\/]bash(\.exe)?$/i.test(shell) ? ["-i"] : [];
133
+ }
134
+ /**
135
+ * Interactive shell for PTYs.
136
+ * - Windows: prefer bash — it matches the SDK's bash tool, so the agent and
137
+ * the terminal speak the same shell language (no more PowerShell/bash
138
+ * 混用 that leaves heredocs / `&&` / `<<` hanging or erroring). Order:
139
+ * 1. PI_WEB_SHELL (explicit override)
140
+ * 2. $SHELL when it exists on disk (user launched from a Git Bash session)
141
+ * 3. Git Bash install paths (ProgramFiles / ProgramFiles(x86))
142
+ * 4. busybox-w32 fallback in <home>/.pi-web/bin/bash.exe (ensure-bash.ts
143
+ * downloads it automatically when 2–3 are absent)
144
+ * 5. $COMSPEC (cmd.exe — always set)
145
+ * 6. powershell.exe (last resort)
146
+ * - POSIX: the user's login shell, falling back to bash.
147
+ * Resolved per terminal spawn (not at module load) so a busybox download that
148
+ * finishes after startup is picked up by the next terminal.
149
+ */
150
+ function resolveShell() {
151
+ if (isWindows) {
152
+ const explicit = process.env.PI_WEB_SHELL;
153
+ if (explicit)
154
+ return { shell: explicit, args: bashArgs(explicit) };
155
+ const she = process.env.SHELL;
156
+ if (she && existsSync(she))
157
+ return { shell: she, args: bashArgs(she) };
158
+ const pf = process.env.ProgramFiles;
159
+ const pf86 = process.env["ProgramFiles(x86)"];
160
+ for (const cand of [
161
+ pf ? join(pf, "Git", "bin", "bash.exe") : "",
162
+ pf86 ? join(pf86, "Git", "bin", "bash.exe") : "",
163
+ ]) {
164
+ if (cand && existsSync(cand))
165
+ return { shell: cand, args: ["-i"] };
166
+ }
167
+ const busybox = join(homedir(), ".pi-web", "bin", "bash.exe");
168
+ if (existsSync(busybox))
169
+ return { shell: busybox, args: ["-i"] };
170
+ return { shell: process.env.COMSPEC || "powershell.exe", args: [] };
171
+ }
172
+ return { shell: process.env.SHELL || "bash", args: ["-i"] };
173
+ }
174
+ /**
175
+ * Shell for the terminal-backed bash tool ('ai-bash'): ALWAYS bash, never the
176
+ * user's login shell (often zsh on macOS, whose `read -p` etc. diverge from
177
+ * the bash semantics models write). Windows already prefers Git Bash/busybox
178
+ * bash; on posix pick $SHELL when it is bash, else plain `bash`.
179
+ */
180
+ function resolveBashShell() {
181
+ if (isWindows) {
182
+ const pf = process.env.ProgramFiles;
183
+ const pf86 = process.env["ProgramFiles(x86)"];
184
+ for (const cand of [
185
+ pf ? join(pf, "Git", "bin", "bash.exe") : "",
186
+ pf86 ? join(pf86, "Git", "bin", "bash.exe") : "",
187
+ ]) {
188
+ if (cand && existsSync(cand))
189
+ return { shell: cand, args: ["-i"] };
190
+ }
191
+ const busybox = join(homedir(), ".pi-web", "bin", "bash.exe");
192
+ if (existsSync(busybox))
193
+ return { shell: busybox, args: ["-i"] };
194
+ }
195
+ const she = process.env.SHELL;
196
+ if (she && she.endsWith("bash") && existsSync(she)) {
197
+ return { shell: she, args: ["-i"] };
198
+ }
199
+ return { shell: "bash", args: ["-i"] };
200
+ }
201
+ /**
202
+ * Environment for spawned shells. System services (launchd/systemd) run with
203
+ * no locale variables, which puts the shell in the C locale: its line editor
204
+ * then renders UTF-8 continuation bytes 0x80–0x9F as C1 control characters
205
+ * (e.g. `�<0091><0098>` for 员), garbling Chinese input in the terminal.
206
+ * Default a UTF-8 locale so multibyte text round-trips.
207
+ */
208
+ function shellEnv() {
209
+ const env = {
210
+ ...process.env,
211
+ TERM: "xterm-256color",
212
+ };
213
+ if (!env.LANG && !env.LC_ALL)
214
+ env.LANG = "en_US.UTF-8";
215
+ return env;
216
+ }
217
+ // ---------------------------------------------------------------------------
218
+ // node-pty ConoutConnection warning noise (Node --watch)
219
+ // ---------------------------------------------------------------------------
220
+ // node-pty's ConoutConnection warns about every unknown message from its ConPTY
221
+ // worker thread. Under `node --watch` (the dev server: `node --watch --import
222
+ // tsx`), Node's watch mode pushes `watch:require` / `watch:import` messages over
223
+ // the worker's message channel to track module dependencies; node-pty doesn't
224
+ // recognize them and logs one `Unexpected ConoutWorkerMessage { … }` per message
225
+ // — hundreds of lines per terminal (the SCM panel's hidden query PTY triggers it
226
+ // on every git-view open). The messages are harmless: the worker only ever sends
227
+ // its READY sentinel, which the handler does process. Filter that exact warning
228
+ // at the console boundary so dev output stays readable. Production runs without
229
+ // --watch and never produces these.
230
+ const originalWarn = console.warn.bind(console);
231
+ console.warn = (...args) => {
232
+ if (args[0] === "Unexpected ConoutWorkerMessage")
233
+ return;
234
+ originalWarn(...args);
235
+ };
236
+ // ---------------------------------------------------------------------------
237
+ // spawn-helper permission repair (node-pty macOS prebuilds)
238
+ // ---------------------------------------------------------------------------
239
+ // node-pty 1.1.0 publishes its macOS prebuilds with `spawn-helper` lacking the
240
+ // execute bit (mode 0644 in the npm tarball), so posix_spawn fails with EACCES
241
+ // and node-pty throws the generic "posix_spawnp failed". Locally-built
242
+ // copies (build/Release) are fine; every `npm install` that picks the prebuild
243
+ // — e.g. `npm i -g pi-harness`, which is what system-service installs run — is
244
+ // broken until the bit is restored. Self-heal at startup AND lazily before
245
+ // every spawn (an `npm i -g` while the server is running replaces the helper
246
+ // under the running process, so the startup-only repair misses it).
247
+ // Best-effort: a read-only node_modules just keeps the old failure.
248
+ const require = createRequire(import.meta.url);
249
+ /** Absolute paths of every node-pty spawn-helper this install can exec. */
250
+ function spawnHelperPaths() {
251
+ try {
252
+ // require.resolve("node-pty") → <pkg>/lib/index.js → package root is two up.
253
+ const pkgDir = dirname(dirname(require.resolve("node-pty")));
254
+ const out = [];
255
+ const built = join(pkgDir, "build", "Release", "spawn-helper");
256
+ if (existsSync(built))
257
+ out.push(built);
258
+ const prebuildsDir = join(pkgDir, "prebuilds");
259
+ if (existsSync(prebuildsDir)) {
260
+ for (const entry of readdirSync(prebuildsDir)) {
261
+ const p = join(prebuildsDir, entry, "spawn-helper");
262
+ if (existsSync(p))
263
+ out.push(p);
264
+ }
265
+ }
266
+ return out;
267
+ }
268
+ catch {
269
+ return [];
270
+ }
271
+ }
272
+ /** Restore the +x bit on node-pty's spawn-helper binaries (idempotent). */
273
+ function repairSpawnHelperPermissions() {
274
+ if (isWindows)
275
+ return;
276
+ for (const p of spawnHelperPaths()) {
277
+ try {
278
+ if ((statSync(p).mode & 0o111) === 0)
279
+ chmodSync(p, 0o755);
280
+ }
281
+ catch {
282
+ // best-effort; a read-only node_modules just keeps the old failure
283
+ }
284
+ }
285
+ }
286
+ repairSpawnHelperPermissions();
287
+ /** Path of a still-broken helper, for the error hint ("" when none). */
288
+ function brokenSpawnHelper() {
289
+ if (isWindows)
290
+ return "";
291
+ for (const p of spawnHelperPaths()) {
292
+ try {
293
+ if ((statSync(p).mode & 0o111) === 0)
294
+ return p;
295
+ }
296
+ catch {
297
+ // ignore
298
+ }
299
+ }
300
+ return "";
301
+ }
302
+ // ---------------------------------------------------------------------------
303
+ // macOS TCC camera/mic warning (launchd-spawned servers)
304
+ // ---------------------------------------------------------------------------
305
+ // TCC attributes camera/mic access to the process chain's "responsible
306
+ // process". When pi-harness runs as a launchd LaunchAgent (node ← launchd),
307
+ // the responsible process is node itself — a bare CLI binary with no app
308
+ // bundle / Info.plist / NSCameraUsageDescription — so TCC silently denies
309
+ // camera access (no prompt, nothing to tick in System Settings) and
310
+ // ffmpeg-style grabbers hang on frame capture. The identical command works
311
+ // from a terminal app that already holds the camera grant. Detect the
312
+ // "no GUI ancestor" case (ppid === 1 on macOS) and warn in the terminal.
313
+ const TCC_HINT = [
314
+ "\x1b[33m[提示] 本终端由后台服务(launchd)启动,macOS 隐私权限(相机/麦克风/屏幕录制等)对此类进程不可用。\x1b[0m",
315
+ "\x1b[90m · 需要隐私权限的命令会被系统静默拒绝:不弹授权窗,系统设置里也无法勾选,表现多为卡死或无输出。",
316
+ " · 这类任务请在你自己已授权的前台终端里运行。",
317
+ " · 本终端内可运行不需要隐私权限的命令(如文件处理、网络请求、远程设备流)。",
318
+ " · 若改在前台终端里运行 pi-harness,本提示即不再出现。\x1b[0m",
319
+ ].join("\r\n") + "\r\n";
320
+ /** True when this server was spawned by launchd (or orphaned) on macOS — no GUI app in the ancestry, so camera/mic TCC grants are unavailable. */
321
+ function launchdSpawnedOnMac() {
322
+ return process.platform === "darwin" && process.ppid === 1;
323
+ }
324
+ /**
325
+ * Translate a logical key (named key or single character) plus modifiers into
326
+ * the exact byte sequence a PTY expects. Named keys are routed by NAME, so a
327
+ * Ctrl/Alt combo is NEVER derived from the key's first letter: Ctrl+ArrowUp
328
+ * must produce `ESC[1;5A`, not Ctrl+A, and Ctrl+Enter `ESC[13;5u`, not Ctrl+E.
329
+ * - arrows / F1–F4 / Home / End keep their plain form when unmodified and
330
+ * gain the xterm modifier parameter (`ESC[1;<m>X`) under Shift/Alt/Ctrl;
331
+ * - other named keys (Enter/Tab/Backspace/Escape/Insert/Delete/PageUp/PageDown)
332
+ * fall back to the CSI-u form (`ESC[<code>;<m>u`) once modified;
333
+ * - plain characters: Ctrl maps A–Z to 0x01–0x1A (error for non-letters),
334
+ * Shift uppercases, Alt prefixes with ESC.
335
+ */
336
+ export function encodeTerminalKey(key, modifiers = {}) {
337
+ const named = {
338
+ Enter: "\r", Return: "\r", Tab: "\t", Backspace: "\x7f", Escape: "\x1b",
339
+ Up: "\x1b[A", ArrowUp: "\x1b[A", Down: "\x1b[B", ArrowDown: "\x1b[B",
340
+ Left: "\x1b[D", ArrowLeft: "\x1b[D", Right: "\x1b[C", ArrowRight: "\x1b[C",
341
+ Home: "\x1b[H", End: "\x1b[F", Delete: "\x1b[3~", Insert: "\x1b[2~",
342
+ PageUp: "\x1b[5~", PageDown: "\x1b[6~", F1: "\x1bOP", F2: "\x1bOQ", F3: "\x1bOR", F4: "\x1bOS",
343
+ };
344
+ let data = named[key] ?? (key.length === 1 ? key : "");
345
+ if (!data)
346
+ return { error: `不支持的终端按键:${key}` };
347
+ // xterm modifier encoding: 1=plain, 2=Shift, 3=Alt, 5=Ctrl,
348
+ // 6=Ctrl+Shift, 7=Ctrl+Alt, 8=Ctrl+Alt+Shift.
349
+ const modifier = 1 + (modifiers.shift ? 1 : 0) + (modifiers.alt ? 2 : 0) + (modifiers.ctrl ? 4 : 0);
350
+ const arrow = /^\x1b\[([A-DHF])$/.exec(data);
351
+ const functionKey = /^\x1bO([P-S])$/.exec(data);
352
+ const namedCode = {
353
+ Enter: 13, Return: 13, Tab: 9, Backspace: 127, Escape: 27,
354
+ Insert: 2, Delete: 3, Home: 1, End: 4, PageUp: 5, PageDown: 6,
355
+ };
356
+ if (arrow && modifier !== 1) {
357
+ data = `\x1b[1;${modifier}${arrow[1]}`;
358
+ }
359
+ else if (functionKey && modifier !== 1) {
360
+ data = `\x1b[1;${modifier}${functionKey[1]}`;
361
+ }
362
+ else if (namedCode[key] !== undefined && modifier !== 1) {
363
+ // CSI-u keeps named keys identifiable. In particular, Ctrl+Enter and
364
+ // Ctrl+Tab must not be derived from the first letter of "Enter"/"Tab".
365
+ data = `\x1b[${namedCode[key]};${modifier}u`;
366
+ }
367
+ else {
368
+ if (modifiers.ctrl) {
369
+ if (key.length !== 1)
370
+ return { error: `Ctrl 组合键无效:${key}` };
371
+ const code = key.toUpperCase().charCodeAt(0);
372
+ if (code >= 64 && code <= 95)
373
+ data = String.fromCharCode(code - 64);
374
+ else
375
+ return { error: `Ctrl 组合键无效:${key}` };
376
+ }
377
+ else if (modifiers.shift && key.length === 1) {
378
+ data = key.toUpperCase();
379
+ }
380
+ if (modifiers.alt)
381
+ data = "\x1b" + data;
382
+ }
383
+ return { data };
384
+ }
385
+ /**
386
+ * Owns one or more PTYs for a conversation. All output is forwarded as
387
+ * `terminal_output` messages through the provided emit (broadcast to every
388
+ * socket attached to the client session). Failed spawns emit an error notice and
389
+ * terminal_exit instead of throwing into the WebSocket dispatcher.
390
+ */
391
+ export class TerminalManager {
392
+ emit;
393
+ workspaceRoot;
394
+ /** Live PTYs only. Exited entries move to history so they no longer consume
395
+ * the live-terminal limit while their output remains readable/replayable. */
396
+ terms = new Map();
397
+ history = new Map();
398
+ seq = 0;
399
+ tccHintShown = false;
400
+ constructor(emit, workspaceRoot) {
401
+ this.emit = emit;
402
+ this.workspaceRoot = workspaceRoot;
403
+ }
404
+ /** Start a plain interactive shell in the given directory. */
405
+ create(id, cwd, cols, rows, fallbackCwd, title, opts) {
406
+ const valid = this.validateId(id);
407
+ if (valid) {
408
+ this.fail(id, valid);
409
+ return null;
410
+ }
411
+ if (this.terms.has(id))
412
+ return this.info(this.terms.get(id));
413
+ // Every spawn path shares the same admission rule (ensureSpawnAllowed):
414
+ // a NEW live PTY needs a free slot under the cap. Reusing an exited name
415
+ // starts a fresh PTY and discards its old history — but only after the
416
+ // slot check, so a rejected request keeps its retained output.
417
+ if (!this.ensureSpawnAllowed(id))
418
+ return null;
419
+ this.history.delete(id);
420
+ const safeCwd = this.safeCwd(cwd || fallbackCwd);
421
+ if (!safeCwd) {
422
+ this.fail(id, "终端工作目录必须位于当前工作区内");
423
+ return null;
424
+ }
425
+ if (this.spawnShell(id, safeCwd, cols, rows, title || `终端 ${++this.seq}`, undefined, opts?.forceBash)) {
426
+ this.maybeEmitTccHint(id);
427
+ this.emitList();
428
+ return this.info(this.terms.get(id));
429
+ }
430
+ return null;
431
+ }
432
+ /** Warn about unavailable camera/mic TCC grants in a fresh terminal, once per client. */
433
+ maybeEmitTccHint(id) {
434
+ if (this.tccHintShown || !launchdSpawnedOnMac())
435
+ return;
436
+ this.tccHintShown = true;
437
+ this.writeOut(id, TCC_HINT);
438
+ }
439
+ /**
440
+ * Start a shell in the command's directory and run the command in it.
441
+ *
442
+ * If a terminal with this id already exists it is RESTARTED in place: the
443
+ * running process is killed and a fresh shell runs the command again in the
444
+ * same terminal (used when re-running a command by clicking its entry).
445
+ */
446
+ runCommand(id, def, cols, rows, pwd) {
447
+ const invalidId = this.validateId(id);
448
+ if (invalidId) {
449
+ this.fail(id, invalidId);
450
+ return;
451
+ }
452
+ const existing = this.terms.get(id);
453
+ // Same admission rule as create(): a live terminal may be restarted in
454
+ // place, but a NEW live PTY needs a free slot — an id sitting in history
455
+ // (exited) does NOT grant one, or re-running exited terminals while at
456
+ // the cap could push the live count past MAX_TERMINALS.
457
+ if (!existing && !this.ensureSpawnAllowed(id))
458
+ return;
459
+ const hasHistory = this.history.has(id);
460
+ const rawDir = resolveCommandCwd(def.cwd, pwd);
461
+ const dir = this.safeCwd(rawDir);
462
+ const command = expandPwd(def.command.trim(), pwd);
463
+ const title = def.name || command || `终端 ${++this.seq}`;
464
+ if (!dir) {
465
+ this.fail(id, "终端工作目录必须位于当前工作区内");
466
+ return;
467
+ }
468
+ if (existing) {
469
+ // Re-run in place: interrupt the current process (kill the PTY's
470
+ // process group) and start a fresh shell with the same id. Keep the
471
+ // last known size so the replacement matches the xterm's dimensions.
472
+ if (!existing.exited) {
473
+ this.flushPending(existing);
474
+ existing.exited = true;
475
+ try {
476
+ existing.pty.kill();
477
+ }
478
+ catch {
479
+ // already dead
480
+ }
481
+ }
482
+ cols = existing.cols || cols;
483
+ rows = existing.rows || rows;
484
+ this.terms.delete(id);
485
+ }
486
+ this.history.delete(id);
487
+ const ok = this.spawnShell(id, dir, cols, rows, title, def);
488
+ if (!ok)
489
+ return;
490
+ this.emitList();
491
+ // Clear the previous run's output, then show a banner and run the command
492
+ // (the PTY input buffer holds it until the shell is ready).
493
+ const banner = "\x1b[2J\x1b[3J\x1b[H" +
494
+ `\x1b[90m> ${command}\x1b[0m \x1b[90m(${dir})\x1b[0m\r\n`;
495
+ const fresh = this.terms.get(id);
496
+ if (fresh)
497
+ this.appendOutput(fresh, banner);
498
+ this.writeOut(id, banner);
499
+ this.maybeEmitTccHint(id);
500
+ if (command)
501
+ this.input(id, command + "\r");
502
+ }
503
+ /** Spawn the user's shell as a PTY. Returns false when the spawn failed. */
504
+ spawnShell(id, cwd, cols, rows, title, command, forceBash) {
505
+ let abs = cwd;
506
+ if (!abs)
507
+ abs = homedir();
508
+ else if (!isAbsolute(abs))
509
+ abs = resolve(abs);
510
+ try {
511
+ if (!existsSync(abs) || !statSync(abs).isDirectory()) {
512
+ this.fail(id, `目录不存在或不是目录:${abs}`);
513
+ return false;
514
+ }
515
+ }
516
+ catch {
517
+ this.fail(id, `无法访问终端目录:${abs}`);
518
+ return false;
519
+ }
520
+ // node-pty's spawn-helper may have lost its +x bit since the last repair
521
+ // (e.g. a global npm install replaced the helper while this server runs).
522
+ repairSpawnHelperPermissions();
523
+ let pty;
524
+ try {
525
+ const { shell, args } = forceBash ? resolveBashShell() : resolveShell();
526
+ pty = spawn(shell, args, {
527
+ name: "xterm-256color",
528
+ cols: Math.max(2, Math.floor(cols) || 80),
529
+ rows: Math.max(2, Math.floor(rows) || 24),
530
+ cwd: abs,
531
+ env: shellEnv(),
532
+ });
533
+ }
534
+ catch (err) {
535
+ const helper = brokenSpawnHelper();
536
+ this.fail(id, helper
537
+ ? `启动终端失败:${err.message}(node-pty 的 spawn-helper 缺少执行权限,请运行:chmod +x "${helper}")`
538
+ : `启动终端失败:${err.message}`);
539
+ return false;
540
+ }
541
+ const entry = {
542
+ id,
543
+ pty,
544
+ title,
545
+ cwd: abs,
546
+ cols: Math.max(2, Math.floor(cols) || 80),
547
+ rows: Math.max(2, Math.floor(rows) || 24),
548
+ exited: false,
549
+ exitCode: null,
550
+ command,
551
+ output: "",
552
+ outputOffset: 0,
553
+ waiters: new Set(),
554
+ pendingOut: "",
555
+ flushTimer: null,
556
+ };
557
+ this.terms.set(id, entry);
558
+ // The closures capture `entry`: after a restart the map points at the
559
+ // replacement, so a late event from the OLD pty must be ignored.
560
+ pty.onData((data) => {
561
+ if (this.terms.get(id) !== entry)
562
+ return;
563
+ this.appendOutput(entry, data);
564
+ // Coalesce instead of emitting per chunk: node-pty onData fires per
565
+ // ConPTY read buffer (dozens of bytes to a few KB each), so a build or
566
+ // `cat` of a big file used to produce hundreds/thousands of WS frames
567
+ // per second — each paying stringify + frame + parse + dispatch cost.
568
+ // A one-frame micro-batch cuts the frame rate 10-50× at ≤16ms latency
569
+ // (imperceptible; xterm writes batch data more efficiently anyway).
570
+ this.queueOut(entry, data);
571
+ });
572
+ pty.onExit(({ exitCode }) => {
573
+ if (this.terms.get(id) !== entry)
574
+ return;
575
+ this.exit(id, exitCode);
576
+ });
577
+ return true;
578
+ }
579
+ /** 武装(或按当前 lastActivityAt 重置)静默看门狗。 */
580
+ /** Emit output immediately, bypassing the coalescing window (rare paths:
581
+ * one-shot hints/banners — not per-chunk data). */
582
+ writeOut(id, data) {
583
+ this.emit({ type: "terminal_output", terminalId: id, data });
584
+ }
585
+ /** Queue output for the coalescing window; flushes via flushPending. */
586
+ queueOut(entry, data) {
587
+ entry.pendingOut += data;
588
+ if (entry.flushTimer)
589
+ return;
590
+ entry.flushTimer = setTimeout(() => {
591
+ entry.flushTimer = null;
592
+ this.flushPending(entry);
593
+ }, OUTPUT_FLUSH_MS);
594
+ }
595
+ /** Emit everything queued for this terminal (no-op when nothing pending). */
596
+ flushPending(entry) {
597
+ if (entry.flushTimer) {
598
+ clearTimeout(entry.flushTimer);
599
+ entry.flushTimer = null;
600
+ }
601
+ if (!entry.pendingOut)
602
+ return;
603
+ const pending = entry.pendingOut;
604
+ entry.pendingOut = "";
605
+ this.emit({ type: "terminal_output", terminalId: entry.id, data: pending });
606
+ }
607
+ appendOutput(entry, data) {
608
+ entry.output += data;
609
+ if (entry.output.length > MAX_OUTPUT) {
610
+ const drop = entry.output.length - MAX_OUTPUT;
611
+ entry.output = entry.output.slice(drop);
612
+ entry.outputOffset += drop;
613
+ }
614
+ for (const wake of entry.waiters)
615
+ wake();
616
+ entry.waiters.clear();
617
+ // 纪元内的输出重置静默倒计时。
618
+ // 输出观察器:累积匹配,命中一次即移除(终端接管 bash 的完成检测)。
619
+ }
620
+ validateId(id) {
621
+ if (!id || id.length > MAX_ID || !/^[A-Za-z0-9._:-]+$/.test(id)) {
622
+ return "终端名称无效:只能使用字母、数字、.-、_ 或 :(最长 80 字符)";
623
+ }
624
+ return null;
625
+ }
626
+ /**
627
+ * Admission control for EVERY spawn path (create / runCommand): spawning a
628
+ * NEW live PTY is only allowed while the live count is below MAX_TERMINALS.
629
+ * Restarting an id that is ALREADY live is always allowed (no extra slot).
630
+ * History entries (exited terminals) do not reserve a slot — re-spawning
631
+ * one while at the cap is rejected with the standard error feedback.
632
+ */
633
+ ensureSpawnAllowed(id) {
634
+ if (this.terms.has(id))
635
+ return true;
636
+ if (this.terms.size >= MAX_TERMINALS) {
637
+ this.fail(id, `终端数量已达上限(${MAX_TERMINALS})`);
638
+ return false;
639
+ }
640
+ return true;
641
+ }
642
+ safeCwd(raw) {
643
+ try {
644
+ const root = realpathSync(resolve(this.workspaceRoot));
645
+ const candidate = realpathSync(isAbsolute(raw) ? resolve(raw) : resolve(root, raw));
646
+ const rel = relative(root, candidate);
647
+ if (rel === "" || (!rel.startsWith(".." + sep) && rel !== ".." && !isAbsolute(rel))) {
648
+ return candidate;
649
+ }
650
+ }
651
+ catch {
652
+ // Missing directories and broken symlinks are rejected by the boundary.
653
+ }
654
+ return null;
655
+ }
656
+ info(entry) {
657
+ return {
658
+ id: entry.id,
659
+ title: entry.title,
660
+ cwd: entry.cwd,
661
+ cols: entry.cols,
662
+ rows: entry.rows,
663
+ running: !entry.exited,
664
+ exitCode: entry.exitCode,
665
+ command: entry.command,
666
+ };
667
+ }
668
+ has(id) {
669
+ return this.terms.has(id) || this.history.has(id);
670
+ }
671
+ find(id) {
672
+ return this.terms.get(id) ?? this.history.get(id);
673
+ }
674
+ list() {
675
+ return [...this.terms.values(), ...this.history.values()].map((entry) => this.info(entry));
676
+ }
677
+ emitList() {
678
+ this.emit({ type: "terminal_list", terminals: this.list() });
679
+ }
680
+ /** Replay the retained output window after switching back to this conversation. */
681
+ replay() {
682
+ return [...this.terms.values(), ...this.history.values()]
683
+ .filter((entry) => entry.output.length > 0)
684
+ .map((entry) => ({ terminalId: entry.id, data: entry.output }));
685
+ }
686
+ /** Read output after an absolute cursor. */
687
+ read(id, cursor = 0, maxBytes = 20_000) {
688
+ const entry = this.find(id);
689
+ if (!entry)
690
+ return null;
691
+ const start = Math.max(entry.outputOffset, Math.min(cursor, entry.outputOffset + entry.output.length));
692
+ const end = Math.min(start + Math.max(1, Math.floor(maxBytes) || 20_000), entry.outputOffset + entry.output.length);
693
+ return { data: entry.output.slice(start - entry.outputOffset, end - entry.outputOffset), cursor: end, running: !entry.exited, exitCode: entry.exitCode };
694
+ }
695
+ async waitForOutput(id, cursor, timeoutMs, signal) {
696
+ const current = this.read(id, cursor, 1);
697
+ if (!current || current.cursor > cursor || !current.running)
698
+ return;
699
+ await new Promise((resolvePromise) => {
700
+ const entry = this.find(id);
701
+ if (!entry)
702
+ return resolvePromise();
703
+ let timer;
704
+ const done = () => {
705
+ if (timer)
706
+ clearTimeout(timer);
707
+ entry.waiters.delete(done);
708
+ signal?.removeEventListener("abort", done);
709
+ resolvePromise();
710
+ };
711
+ entry.waiters.add(done);
712
+ timer = setTimeout(done, Math.max(0, Math.min(timeoutMs, 120_000)));
713
+ signal?.addEventListener("abort", done, { once: true });
714
+ });
715
+ }
716
+ inputChecked(id, data) {
717
+ if (data.length > MAX_INPUT)
718
+ return `输入过长(上限 ${MAX_INPUT} 字符)`;
719
+ const entry = this.terms.get(id);
720
+ if (!entry || entry.exited)
721
+ return "终端不存在或进程已退出";
722
+ // 已武装的纪元里任何人(含用户手动敲键盘)写了输入都算新活动,重置倒计时。
723
+ entry.pty.write(data);
724
+ return null;
725
+ }
726
+ key(id, key, modifiers = {}) {
727
+ const encoded = encodeTerminalKey(key, modifiers);
728
+ if ("error" in encoded)
729
+ return encoded.error;
730
+ return this.inputChecked(id, encoded.data);
731
+ }
732
+ /** 解除静默看门狗(退出/关闭/全部停止时)。 */
733
+ /** 输出末尾的绝对 cursor(terminal-backed bash 的读取起点)。 */
734
+ endCursor(id) {
735
+ const entry = this.find(id);
736
+ if (!entry)
737
+ return null;
738
+ return entry.outputOffset + entry.output.length;
739
+ }
740
+ /** 注册一次性输出观察器:命中 re 或终端退出时回调一次。返回注销函数。 */
741
+ /** Emit a terminal failure (bad cwd, spawn error) and mark the terminal dead. */
742
+ fail(id, text) {
743
+ this.emit({ type: "notice", level: "error", text });
744
+ this.emit({
745
+ type: "terminal_output",
746
+ terminalId: id,
747
+ data: `\x1b[91m${text}\x1b[0m\r\n`,
748
+ });
749
+ this.emit({ type: "terminal_exit", terminalId: id, exitCode: null });
750
+ }
751
+ exit(id, exitCode) {
752
+ const entry = this.terms.get(id);
753
+ if (!entry || entry.exited)
754
+ return;
755
+ // Flush queued output BEFORE the exit banner so ordering is preserved.
756
+ this.flushPending(entry);
757
+ const banner = `\r\n\x1b[90m[进程已退出,退出码 ${exitCode}]\x1b[0m\r\n`;
758
+ this.appendOutput(entry, banner);
759
+ this.emit({ type: "terminal_output", terminalId: id, data: banner });
760
+ entry.exited = true;
761
+ // 终端退出 → 未命中的输出观察器以 null 回调(宿主可据此通知「终端已关闭」)。
762
+ entry.exitCode = exitCode;
763
+ this.terms.delete(id);
764
+ while (this.history.size >= MAX_TERMINAL_HISTORY) {
765
+ const oldest = this.history.keys().next().value;
766
+ if (typeof oldest !== "string")
767
+ break;
768
+ this.history.delete(oldest);
769
+ }
770
+ this.history.set(id, entry);
771
+ this.emit({ type: "terminal_exit", terminalId: id, exitCode });
772
+ this.emitList();
773
+ }
774
+ input(id, data) {
775
+ void this.inputChecked(id, data);
776
+ }
777
+ resize(id, cols, rows) {
778
+ const entry = this.terms.get(id);
779
+ if (!entry || entry.exited)
780
+ return;
781
+ try {
782
+ entry.pty.resize(Math.max(2, Math.floor(cols) || 80), Math.max(2, Math.floor(rows) || 24));
783
+ // Remember the size so an in-place restart spawns at the same dims.
784
+ entry.cols = Math.max(2, Math.floor(cols) || 80);
785
+ entry.rows = Math.max(2, Math.floor(rows) || 24);
786
+ }
787
+ catch {
788
+ // PTY already gone — nothing to do.
789
+ }
790
+ }
791
+ /** Kill one terminal (tab closed), including an exited terminal's retained history. */
792
+ kill(id) {
793
+ const entry = this.terms.get(id);
794
+ if (entry) {
795
+ this.flushPending(entry);
796
+ entry.exited = true;
797
+ try {
798
+ entry.pty.kill();
799
+ }
800
+ catch {
801
+ // already dead
802
+ }
803
+ this.terms.delete(id);
804
+ this.emit({ type: "terminal_exit", terminalId: id, exitCode: null });
805
+ this.emitList();
806
+ return;
807
+ }
808
+ if (this.history.delete(id))
809
+ this.emitList();
810
+ }
811
+ /** Kill every terminal owned by this conversation. */
812
+ killAll() {
813
+ for (const entry of this.terms.values()) {
814
+ if (entry.exited)
815
+ continue;
816
+ entry.exited = true;
817
+ try {
818
+ entry.pty.kill();
819
+ }
820
+ catch {
821
+ // already dead
822
+ }
823
+ }
824
+ for (const entry of this.terms.values()) {
825
+ for (const wake of entry.waiters)
826
+ wake();
827
+ entry.waiters.clear();
828
+ }
829
+ this.terms.clear();
830
+ this.history.clear();
831
+ this.emitList();
832
+ }
833
+ }