@youweichen/pi-harness 0.0.0-stage → 1.1.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 (456) 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 +3131 -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 +178 -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 +188 -0
  30. package/dist/server/files-service.js +787 -0
  31. package/dist/server/index.js +1033 -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-agent.js +262 -0
  40. package/dist/server/node-sources.js +142 -0
  41. package/dist/server/node-workbench.js +729 -0
  42. package/dist/server/origin-policy.js +50 -0
  43. package/dist/server/patch-node-pty.js +96 -0
  44. package/dist/server/pi-environment.js +203 -0
  45. package/dist/server/plan/extension.js +74 -0
  46. package/dist/server/plan/progress.js +90 -0
  47. package/dist/server/plan/settings.js +41 -0
  48. package/dist/server/plan/state.js +123 -0
  49. package/dist/server/plugin-facilities.js +304 -0
  50. package/dist/server/plugin-updater.js +245 -0
  51. package/dist/server/plugins.js +712 -0
  52. package/dist/server/private-file.js +28 -0
  53. package/dist/server/process-utils.js +112 -0
  54. package/dist/server/prompt-delivery.js +80 -0
  55. package/dist/server/protocol-version.js +11 -0
  56. package/dist/server/protocol.js +6 -0
  57. package/dist/server/provider-auth.js +99 -0
  58. package/dist/server/query-cache.js +45 -0
  59. package/dist/server/recovery-state.js +37 -0
  60. package/dist/server/scm.js +459 -0
  61. package/dist/server/security-headers.js +24 -0
  62. package/dist/server/serialize.js +254 -0
  63. package/dist/server/session-file-read.js +225 -0
  64. package/dist/server/session-record-worker.js +21 -0
  65. package/dist/server/session-reload.js +13 -0
  66. package/dist/server/session-tree-controller.js +250 -0
  67. package/dist/server/session-tree.js +191 -0
  68. package/dist/server/settings-service.js +53 -0
  69. package/dist/server/skills-service.js +87 -0
  70. package/dist/server/slash-commands.js +286 -0
  71. package/dist/server/sqlite-preview.js +58 -0
  72. package/dist/server/sqlite-query.js +67 -0
  73. package/dist/server/sqlite-worker.js +12 -0
  74. package/dist/server/system-prompt-files.js +158 -0
  75. package/dist/server/system-prompt-routes.js +40 -0
  76. package/dist/server/system-prompt-view.js +49 -0
  77. package/dist/server/task-progress.js +233 -0
  78. package/dist/server/terminals.js +833 -0
  79. package/dist/server/text-sniff.js +269 -0
  80. package/dist/server/thinking-timing.js +91 -0
  81. package/dist/server/token-auth.js +37 -0
  82. package/dist/server/tool-output.js +97 -0
  83. package/dist/server/tool-text-incidents.js +82 -0
  84. package/dist/server/tool-text.js +67 -0
  85. package/dist/server/uploads.js +112 -0
  86. package/dist/server/user-attachments.js +96 -0
  87. package/dist/server/webui-context.js +257 -0
  88. package/dist/server/wiki-links.js +136 -0
  89. package/dist/server/wiki-pdf-worker.js +23 -0
  90. package/dist/server/wiki-pdf.js +36 -0
  91. package/dist/server/wiki-routes.js +184 -0
  92. package/dist/server/wiki-service.js +524 -0
  93. package/electron/main.mjs +607 -0
  94. package/extensions/webui.ts +192 -0
  95. package/package.json +122 -4
  96. package/web/dist/apple-touch-icon.png +0 -0
  97. package/web/dist/assets/NodeWorkbench-BiP2ktQP.js +3 -0
  98. package/web/dist/assets/TerminalPanel-CKaSUsN-.js +2 -0
  99. package/web/dist/assets/abnfDiagram-VCTEODGH-CnwRJDRL.js +1 -0
  100. package/web/dist/assets/arc-CG4Q7-W_.js +1 -0
  101. package/web/dist/assets/architectureDiagram-5GKGNRK7-BrGqX_Rg.js +36 -0
  102. package/web/dist/assets/blockDiagram-I7D4REHJ-C2t0PjZT.js +129 -0
  103. package/web/dist/assets/c4Diagram-7LVT6UL2-CadrcgGL.js +38 -0
  104. package/web/dist/assets/channel-C27Jizb6.js +1 -0
  105. package/web/dist/assets/chunk-2Q5K7J3B-CC3V2ITZ.js +1 -0
  106. package/web/dist/assets/chunk-5VM5RSS4-DqKSbGmE.js +15 -0
  107. package/web/dist/assets/chunk-F27PBJKO-_CDr3JbY.js +1 -0
  108. package/web/dist/assets/chunk-IMKFNOWR-BIR4H4fX.js +231 -0
  109. package/web/dist/assets/chunk-JWPE2WC7-CrsdBGXT.js +1 -0
  110. package/web/dist/assets/chunk-POPQ4Y6H-C6q9kQmf.js +1 -0
  111. package/web/dist/assets/chunk-SVP7TREG-l2XXTr2v.js +88 -0
  112. package/web/dist/assets/chunk-TICWLB2K-29I_mZgB.js +206 -0
  113. package/web/dist/assets/chunk-XXDRQBXY-htV9FBqi.js +1 -0
  114. package/web/dist/assets/classDiagram-ZZMXUADV-DYTBrAEP.js +1 -0
  115. package/web/dist/assets/classDiagram-v2-VYDZK3BY-DYTBrAEP.js +1 -0
  116. package/web/dist/assets/cose-bilkent-JH36ORCC-Cop-ihyx.js +1 -0
  117. package/web/dist/assets/cynefin-OW5HDTMX-IEk8qBRq.js +178 -0
  118. package/web/dist/assets/cynefinDiagram-5FMLGOSQ-Bmzt6Qb5.js +62 -0
  119. package/web/dist/assets/cytoscape.esm-Bch-eiPH.js +331 -0
  120. package/web/dist/assets/dagre-GXQ25YYZ-DiXTJYYU.js +4 -0
  121. package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  122. package/web/dist/assets/diagram-S7CK7UJ4-BT7CLuNk.js +30 -0
  123. package/web/dist/assets/diagram-UQ7AKVKN-BZZ3nTCO.js +41 -0
  124. package/web/dist/assets/diagram-VSXAHHWV-Dal8LeZh.js +3 -0
  125. package/web/dist/assets/diagram-VX7I27RA-D6V9FfNA.js +24 -0
  126. package/web/dist/assets/diagram-Z3DM3KII-KUAIcFiD.js +24 -0
  127. package/web/dist/assets/ebnfDiagram-PWID7BFC-s0FEPZrV.js +1 -0
  128. package/web/dist/assets/erDiagram-RLTQ6QDP-CiIQPFCD.js +99 -0
  129. package/web/dist/assets/flowDiagram-HODETNUW-CvG4MYWn.js +168 -0
  130. package/web/dist/assets/ganttDiagram-EL5Y4UJY-CUf115ii.js +292 -0
  131. package/web/dist/assets/gitGraphDiagram-WWUBYQGX-o3K1r6fL.js +106 -0
  132. package/web/dist/assets/index-C3vDFSEV.css +10 -0
  133. package/web/dist/assets/index-C5or_GAq.js +276 -0
  134. package/web/dist/assets/infoDiagram-27XIBGKW-BxiBeQ8E.js +2 -0
  135. package/web/dist/assets/init-Gi6I4Gst.js +1 -0
  136. package/web/dist/assets/ishikawaDiagram-5VMMS53U-B1ONuCkH.js +70 -0
  137. package/web/dist/assets/journeyDiagram-3NMN7TZE-DwmuoXcX.js +139 -0
  138. package/web/dist/assets/kanban-definition-UXKFOSKX-pMKHubxH.js +89 -0
  139. package/web/dist/assets/katex-C5jXJg4s.js +257 -0
  140. package/web/dist/assets/layout-CEwfjQR0.js +1 -0
  141. package/web/dist/assets/linear-D22Gdbt1.js +1 -0
  142. package/web/dist/assets/markdown-BhJ_LInD.js +51 -0
  143. package/web/dist/assets/mermaid.core-CO2nrD04.js +312 -0
  144. package/web/dist/assets/mindmap-definition-YA3MSWOX-9MQn9ZAp.js +96 -0
  145. package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  146. package/web/dist/assets/pegDiagram-XKGWAZYB-Dy6fLjM-.js +1 -0
  147. package/web/dist/assets/pieDiagram-E7YTZNPT-EDLez6nA.js +39 -0
  148. package/web/dist/assets/quadrantDiagram-AXDQQJYC-Cgcd9mvy.js +7 -0
  149. package/web/dist/assets/railroadDiagram-O6MQD6OU-B4Td0tjj.js +1 -0
  150. package/web/dist/assets/react-BddUxV-9.js +24 -0
  151. package/web/dist/assets/requirementDiagram-BXWQKSXE-aF2QNZ78.js +84 -0
  152. package/web/dist/assets/sankeyDiagram-P5KCCOFB-DgTYwYMa.js +40 -0
  153. package/web/dist/assets/sequenceDiagram-WJ2MYXX4-EUcddWUc.js +162 -0
  154. package/web/dist/assets/sizeCapture-INFHLROL-CXHOv9Sx.js +1 -0
  155. package/web/dist/assets/stateDiagram-D77RDMKH-B-xlgfaK.js +1 -0
  156. package/web/dist/assets/stateDiagram-v2-MP3YSRHH-kwAwAlWX.js +1 -0
  157. package/web/dist/assets/swimlanes-42K2YHIH-BtFBOCDh.js +1 -0
  158. package/web/dist/assets/swimlanesDiagram-VR7AAH4N-D5j5Cj3S.js +8 -0
  159. package/web/dist/assets/timeline-definition-24CTP7MA-CZlQY6pb.js +120 -0
  160. package/web/dist/assets/vennDiagram-4TSXK5OY-Cn61dJvL.js +34 -0
  161. package/web/dist/assets/wardleyDiagram-VM6X3IG4-fiMNrWpr.js +78 -0
  162. package/web/dist/assets/xterm-6GBZ9nXN.css +32 -0
  163. package/web/dist/assets/xterm-D1D2FVe3.js +38 -0
  164. package/web/dist/assets/xychartDiagram-S5SC5T6Z-CuzM_sof.js +7 -0
  165. package/web/dist/brand-mark.svg +5 -0
  166. package/web/dist/favicon-32.png +0 -0
  167. package/web/dist/favicon.svg +5 -0
  168. package/web/dist/fonts/WIKI-FONTS.md +5 -0
  169. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  170. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  171. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  172. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
  173. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
  174. package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-600-normal.woff2 +0 -0
  175. package/web/dist/fonts/ibm-plex-sans/LICENSE +93 -0
  176. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  177. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  178. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  179. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
  180. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
  181. package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
  182. package/web/dist/fonts/inter/inter-latin-400-normal.woff2 +0 -0
  183. package/web/dist/fonts/inter/inter-latin-500-normal.woff2 +0 -0
  184. package/web/dist/fonts/inter/inter-latin-600-normal.woff2 +0 -0
  185. package/web/dist/fonts/inter/inter-latin-700-normal.woff2 +0 -0
  186. package/web/dist/fonts/inter/inter-latin-800-normal.woff2 +0 -0
  187. package/web/dist/fonts/inter/inter-latin-ext-400-normal.woff2 +0 -0
  188. package/web/dist/fonts/inter/inter-latin-ext-500-normal.woff2 +0 -0
  189. package/web/dist/fonts/inter/inter-latin-ext-600-normal.woff2 +0 -0
  190. package/web/dist/fonts/inter/inter-latin-ext-700-normal.woff2 +0 -0
  191. package/web/dist/fonts/inter/inter-latin-ext-800-normal.woff2 +0 -0
  192. package/web/dist/fonts/jetbrains-mono/LICENSE +93 -0
  193. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-400-normal.woff2 +0 -0
  194. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-500-normal.woff2 +0 -0
  195. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-600-normal.woff2 +0 -0
  196. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
  197. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
  198. package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-600-normal.woff2 +0 -0
  199. package/web/dist/fonts/noto-serif-sc/LICENSE +94 -0
  200. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-0.woff2 +0 -0
  201. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-1.woff2 +0 -0
  202. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-10.woff2 +0 -0
  203. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-100.woff2 +0 -0
  204. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-11.woff2 +0 -0
  205. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-12.woff2 +0 -0
  206. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-13.woff2 +0 -0
  207. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-14.woff2 +0 -0
  208. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-15.woff2 +0 -0
  209. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-16.woff2 +0 -0
  210. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-17.woff2 +0 -0
  211. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-18.woff2 +0 -0
  212. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-19.woff2 +0 -0
  213. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-2.woff2 +0 -0
  214. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-20.woff2 +0 -0
  215. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-21.woff2 +0 -0
  216. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-22.woff2 +0 -0
  217. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-23.woff2 +0 -0
  218. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-24.woff2 +0 -0
  219. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-25.woff2 +0 -0
  220. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-26.woff2 +0 -0
  221. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-27.woff2 +0 -0
  222. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-28.woff2 +0 -0
  223. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-29.woff2 +0 -0
  224. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-3.woff2 +0 -0
  225. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-30.woff2 +0 -0
  226. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-31.woff2 +0 -0
  227. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-32.woff2 +0 -0
  228. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-33.woff2 +0 -0
  229. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-34.woff2 +0 -0
  230. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-35.woff2 +0 -0
  231. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-36.woff2 +0 -0
  232. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-37.woff2 +0 -0
  233. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-38.woff2 +0 -0
  234. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-39.woff2 +0 -0
  235. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-4.woff2 +0 -0
  236. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-40.woff2 +0 -0
  237. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-41.woff2 +0 -0
  238. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-42.woff2 +0 -0
  239. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-43.woff2 +0 -0
  240. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-44.woff2 +0 -0
  241. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-45.woff2 +0 -0
  242. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-46.woff2 +0 -0
  243. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-47.woff2 +0 -0
  244. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-48.woff2 +0 -0
  245. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-49.woff2 +0 -0
  246. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-5.woff2 +0 -0
  247. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-50.woff2 +0 -0
  248. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-51.woff2 +0 -0
  249. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-52.woff2 +0 -0
  250. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-53.woff2 +0 -0
  251. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-54.woff2 +0 -0
  252. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-55.woff2 +0 -0
  253. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-56.woff2 +0 -0
  254. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-57.woff2 +0 -0
  255. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-58.woff2 +0 -0
  256. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-59.woff2 +0 -0
  257. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-6.woff2 +0 -0
  258. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-60.woff2 +0 -0
  259. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-61.woff2 +0 -0
  260. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-62.woff2 +0 -0
  261. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-63.woff2 +0 -0
  262. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-64.woff2 +0 -0
  263. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-65.woff2 +0 -0
  264. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-66.woff2 +0 -0
  265. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-67.woff2 +0 -0
  266. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-68.woff2 +0 -0
  267. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-69.woff2 +0 -0
  268. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-7.woff2 +0 -0
  269. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-70.woff2 +0 -0
  270. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-71.woff2 +0 -0
  271. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-72.woff2 +0 -0
  272. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-73.woff2 +0 -0
  273. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-74.woff2 +0 -0
  274. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-75.woff2 +0 -0
  275. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-76.woff2 +0 -0
  276. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-77.woff2 +0 -0
  277. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-78.woff2 +0 -0
  278. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-79.woff2 +0 -0
  279. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-8.woff2 +0 -0
  280. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-80.woff2 +0 -0
  281. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-81.woff2 +0 -0
  282. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-82.woff2 +0 -0
  283. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-83.woff2 +0 -0
  284. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-84.woff2 +0 -0
  285. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-85.woff2 +0 -0
  286. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-86.woff2 +0 -0
  287. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-87.woff2 +0 -0
  288. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-88.woff2 +0 -0
  289. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-89.woff2 +0 -0
  290. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-9.woff2 +0 -0
  291. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-90.woff2 +0 -0
  292. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-91.woff2 +0 -0
  293. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-92.woff2 +0 -0
  294. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-93.woff2 +0 -0
  295. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-94.woff2 +0 -0
  296. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-95.woff2 +0 -0
  297. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-96.woff2 +0 -0
  298. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-97.woff2 +0 -0
  299. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-98.woff2 +0 -0
  300. package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-99.woff2 +0 -0
  301. package/web/dist/fonts/source-serif-4/LICENSE +93 -0
  302. package/web/dist/fonts/source-serif-4/source-serif-4-500-0.woff2 +0 -0
  303. package/web/dist/fonts/source-serif-4/source-serif-4-500-1.woff2 +0 -0
  304. package/web/dist/fonts/source-serif-4/source-serif-4-500-2.woff2 +0 -0
  305. package/web/dist/fonts/source-serif-4/source-serif-4-500-3.woff2 +0 -0
  306. package/web/dist/fonts/source-serif-4/source-serif-4-500-4.woff2 +0 -0
  307. package/web/dist/fonts/source-serif-4/source-serif-4-500-5.woff2 +0 -0
  308. package/web/dist/icon/1a-mark.svg +1 -0
  309. package/web/dist/icon.ico +0 -0
  310. package/web/dist/index.html +18 -0
  311. package/web/public/apple-touch-icon.png +0 -0
  312. package/web/public/brand-mark.svg +5 -0
  313. package/web/public/favicon-32.png +0 -0
  314. package/web/public/favicon.svg +5 -0
  315. package/web/public/fonts/WIKI-FONTS.md +5 -0
  316. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  317. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  318. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  319. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
  320. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
  321. package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-600-normal.woff2 +0 -0
  322. package/web/public/fonts/ibm-plex-sans/LICENSE +93 -0
  323. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  324. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  325. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  326. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
  327. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
  328. package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
  329. package/web/public/fonts/inter/inter-latin-400-normal.woff2 +0 -0
  330. package/web/public/fonts/inter/inter-latin-500-normal.woff2 +0 -0
  331. package/web/public/fonts/inter/inter-latin-600-normal.woff2 +0 -0
  332. package/web/public/fonts/inter/inter-latin-700-normal.woff2 +0 -0
  333. package/web/public/fonts/inter/inter-latin-800-normal.woff2 +0 -0
  334. package/web/public/fonts/inter/inter-latin-ext-400-normal.woff2 +0 -0
  335. package/web/public/fonts/inter/inter-latin-ext-500-normal.woff2 +0 -0
  336. package/web/public/fonts/inter/inter-latin-ext-600-normal.woff2 +0 -0
  337. package/web/public/fonts/inter/inter-latin-ext-700-normal.woff2 +0 -0
  338. package/web/public/fonts/inter/inter-latin-ext-800-normal.woff2 +0 -0
  339. package/web/public/fonts/jetbrains-mono/LICENSE +93 -0
  340. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-400-normal.woff2 +0 -0
  341. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-500-normal.woff2 +0 -0
  342. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-600-normal.woff2 +0 -0
  343. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
  344. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
  345. package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-600-normal.woff2 +0 -0
  346. package/web/public/fonts/noto-serif-sc/LICENSE +94 -0
  347. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-0.woff2 +0 -0
  348. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-1.woff2 +0 -0
  349. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-10.woff2 +0 -0
  350. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-100.woff2 +0 -0
  351. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-11.woff2 +0 -0
  352. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-12.woff2 +0 -0
  353. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-13.woff2 +0 -0
  354. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-14.woff2 +0 -0
  355. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-15.woff2 +0 -0
  356. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-16.woff2 +0 -0
  357. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-17.woff2 +0 -0
  358. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-18.woff2 +0 -0
  359. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-19.woff2 +0 -0
  360. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-2.woff2 +0 -0
  361. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-20.woff2 +0 -0
  362. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-21.woff2 +0 -0
  363. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-22.woff2 +0 -0
  364. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-23.woff2 +0 -0
  365. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-24.woff2 +0 -0
  366. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-25.woff2 +0 -0
  367. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-26.woff2 +0 -0
  368. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-27.woff2 +0 -0
  369. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-28.woff2 +0 -0
  370. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-29.woff2 +0 -0
  371. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-3.woff2 +0 -0
  372. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-30.woff2 +0 -0
  373. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-31.woff2 +0 -0
  374. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-32.woff2 +0 -0
  375. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-33.woff2 +0 -0
  376. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-34.woff2 +0 -0
  377. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-35.woff2 +0 -0
  378. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-36.woff2 +0 -0
  379. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-37.woff2 +0 -0
  380. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-38.woff2 +0 -0
  381. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-39.woff2 +0 -0
  382. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-4.woff2 +0 -0
  383. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-40.woff2 +0 -0
  384. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-41.woff2 +0 -0
  385. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-42.woff2 +0 -0
  386. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-43.woff2 +0 -0
  387. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-44.woff2 +0 -0
  388. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-45.woff2 +0 -0
  389. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-46.woff2 +0 -0
  390. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-47.woff2 +0 -0
  391. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-48.woff2 +0 -0
  392. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-49.woff2 +0 -0
  393. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-5.woff2 +0 -0
  394. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-50.woff2 +0 -0
  395. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-51.woff2 +0 -0
  396. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-52.woff2 +0 -0
  397. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-53.woff2 +0 -0
  398. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-54.woff2 +0 -0
  399. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-55.woff2 +0 -0
  400. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-56.woff2 +0 -0
  401. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-57.woff2 +0 -0
  402. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-58.woff2 +0 -0
  403. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-59.woff2 +0 -0
  404. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-6.woff2 +0 -0
  405. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-60.woff2 +0 -0
  406. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-61.woff2 +0 -0
  407. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-62.woff2 +0 -0
  408. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-63.woff2 +0 -0
  409. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-64.woff2 +0 -0
  410. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-65.woff2 +0 -0
  411. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-66.woff2 +0 -0
  412. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-67.woff2 +0 -0
  413. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-68.woff2 +0 -0
  414. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-69.woff2 +0 -0
  415. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-7.woff2 +0 -0
  416. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-70.woff2 +0 -0
  417. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-71.woff2 +0 -0
  418. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-72.woff2 +0 -0
  419. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-73.woff2 +0 -0
  420. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-74.woff2 +0 -0
  421. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-75.woff2 +0 -0
  422. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-76.woff2 +0 -0
  423. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-77.woff2 +0 -0
  424. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-78.woff2 +0 -0
  425. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-79.woff2 +0 -0
  426. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-8.woff2 +0 -0
  427. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-80.woff2 +0 -0
  428. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-81.woff2 +0 -0
  429. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-82.woff2 +0 -0
  430. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-83.woff2 +0 -0
  431. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-84.woff2 +0 -0
  432. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-85.woff2 +0 -0
  433. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-86.woff2 +0 -0
  434. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-87.woff2 +0 -0
  435. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-88.woff2 +0 -0
  436. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-89.woff2 +0 -0
  437. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-9.woff2 +0 -0
  438. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-90.woff2 +0 -0
  439. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-91.woff2 +0 -0
  440. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-92.woff2 +0 -0
  441. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-93.woff2 +0 -0
  442. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-94.woff2 +0 -0
  443. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-95.woff2 +0 -0
  444. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-96.woff2 +0 -0
  445. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-97.woff2 +0 -0
  446. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-98.woff2 +0 -0
  447. package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-99.woff2 +0 -0
  448. package/web/public/fonts/source-serif-4/LICENSE +93 -0
  449. package/web/public/fonts/source-serif-4/source-serif-4-500-0.woff2 +0 -0
  450. package/web/public/fonts/source-serif-4/source-serif-4-500-1.woff2 +0 -0
  451. package/web/public/fonts/source-serif-4/source-serif-4-500-2.woff2 +0 -0
  452. package/web/public/fonts/source-serif-4/source-serif-4-500-3.woff2 +0 -0
  453. package/web/public/fonts/source-serif-4/source-serif-4-500-4.woff2 +0 -0
  454. package/web/public/fonts/source-serif-4/source-serif-4-500-5.woff2 +0 -0
  455. package/web/public/icon/1a-mark.svg +1 -0
  456. package/web/public/icon.ico +0 -0
@@ -0,0 +1,787 @@
1
+ /**
2
+ * Files service — 从 agent-service.ts 抽出(文件树列目录 / 预览读写 / 路径补全 /
3
+ * SCM 只读查询 / 目录与 git-dir watcher)。
4
+ *
5
+ * 全部为无状态 fs 操作 + 两个自持的 watcher(当前列出目录、git dir),
6
+ * 经 FilesHost 回调与 ClientSession 解耦。
7
+ */
8
+ import { createHash } from "node:crypto";
9
+ import { readFileSync, realpathSync, statSync, writeFileSync, watch } from "node:fs";
10
+ import { isAbsolute, resolve, relative, sep } from "node:path";
11
+ import { previewKind, looksLikeText, decodeText, hexDump, countLines, } from "./text-sniff.js";
12
+ import { gitDirOf, isNotRepoError, scmStatus, scmCurrentBranch, scmHistory, scmFileDiff, scmDiff, scmCommitDetail, } from "./scm.js";
13
+ export const IS_WIN32 = process.platform === "win32";
14
+ /** 预览只读文件前 512KB。 */
15
+ export const MAX_PREVIEW_BYTES = 512 * 1024;
16
+ /** Confirm history-derived paths exist as files inside this workspace. */
17
+ export function existingConversationFiles(cwd, paths) {
18
+ let root;
19
+ try {
20
+ root = realpathSync(cwd);
21
+ }
22
+ catch {
23
+ return [];
24
+ }
25
+ const seen = new Set();
26
+ const found = [];
27
+ for (const path of paths.slice(0, 64)) {
28
+ if (typeof path !== "string" || path.length > 4096 || path.includes("\0") || seen.has(path))
29
+ continue;
30
+ seen.add(path);
31
+ const normalized = path.replaceAll("\\", "/");
32
+ if (normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized) || normalized.split("/").includes(".."))
33
+ continue;
34
+ try {
35
+ const target = realpathSync(resolve(root, normalized));
36
+ const rel = relative(root, target);
37
+ if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || resolve(root, rel) !== target)
38
+ continue;
39
+ if (statSync(target).isFile())
40
+ found.push(path);
41
+ }
42
+ catch { /* removed, unreadable, or invalid */ }
43
+ }
44
+ return found;
45
+ }
46
+ // mac/linux: hide build & dependency noise (original behavior).
47
+ const IGNORED_ENTRIES = new Set([
48
+ "node_modules",
49
+ ".git",
50
+ ".svn",
51
+ ".hg",
52
+ "dist",
53
+ ".next",
54
+ ".nuxt",
55
+ ".cache",
56
+ ".venv",
57
+ "venv",
58
+ "__pycache__",
59
+ "coverage",
60
+ ".pi-web",
61
+ ".DS_Store",
62
+ "Thumbs.db",
63
+ ]);
64
+ // Windows: the file tree is the primary way to navigate a project, so only
65
+ // hide what would flood or destabilize the panel (dependency trees, VCS
66
+ // internals, session data) plus pure junk. Build output (dist/.next/…) and
67
+ // local env dirs (venv/__pycache__/…) stay visible — "所有文件可查看".
68
+ const IGNORED_ENTRIES_WIN = new Set([
69
+ "node_modules",
70
+ ".git",
71
+ ".pi-web",
72
+ ".DS_Store",
73
+ "Thumbs.db",
74
+ "desktop.ini",
75
+ ]);
76
+ /** The ignore set for the current platform — keeps win/posix lists separate. */
77
+ function ignoredEntries() {
78
+ return IS_WIN32 ? IGNORED_ENTRIES_WIN : IGNORED_ENTRIES;
79
+ }
80
+ /**
81
+ * Resolve a workspace-relative path against a root, refusing traversal
82
+ * (".." escapes). Returns { abs, rel } — rel is normalized and slash-
83
+ * separated — or null when the path leaves the workspace.
84
+ */
85
+ export function workspacePath(root, raw) {
86
+ const abs = resolve(root, raw);
87
+ const rawRel = relative(root, abs);
88
+ // relative() yields an absolute path across Windows drives / UNC shares, and
89
+ // "..foo" is a legitimate file name — only a real ".." segment escapes.
90
+ if (isAbsolute(rawRel) || rawRel === ".." || rawRel.startsWith(`..${sep}`))
91
+ return null;
92
+ // Normalize to forward slashes: the wire protocol and the frontend always
93
+ // use "/", but relative() returns "\\" on Windows.
94
+ return { abs, rel: rawRel.split(sep).join("/") };
95
+ }
96
+ /**
97
+ * Symlink-aware containment: a link inside the workspace that points outside
98
+ * must not expose the target. A missing file is left to the caller's own
99
+ * stat/open to report.
100
+ */
101
+ export function isRealPathInWorkspace(root, abs) {
102
+ try {
103
+ const rel = relative(realpathSync(root), realpathSync(abs));
104
+ return !(isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`));
105
+ }
106
+ catch (err) {
107
+ return err.code === "ENOENT";
108
+ }
109
+ }
110
+ /**
111
+ * Read a directory for the file panel. The two platforms intentionally use
112
+ * different strategies — do NOT unify them:
113
+ *
114
+ * darwin/linux (posix): original behavior — hide build/dependency noise,
115
+ * small cap, hard error notice when the directory itself is unreadable.
116
+ *
117
+ * win32: stability and completeness first, preview second. ACL-protected
118
+ * system dirs (C:\$Recycle.Bin, Program Files internals, OneDrive placeholders)
119
+ * throw EPERM/EACCES on open — that must not kill the panel, so it degrades
120
+ * to an empty listing with a node-local error. Directory symlinks/junctions are
121
+ * followed so mklink /D folders stay navigable; broken links still show as
122
+ * files instead of vanishing. The cap is 4x posix and truncation is reported
123
+ * via `truncated` instead of happening silently.
124
+ */
125
+ async function readDirForUI(abs, rel) {
126
+ const { join } = await import("node:path");
127
+ const fs = await import("node:fs/promises");
128
+ const ignored = ignoredEntries();
129
+ const MAX = IS_WIN32 ? 2000 : 500;
130
+ let dirents;
131
+ try {
132
+ dirents = await fs.readdir(abs, { withFileTypes: true });
133
+ }
134
+ catch (err) {
135
+ const code = err.code;
136
+ return { entries: [], truncated: false, error: {
137
+ code: code === "ENOENT" ? "missing" : code === "ENOTDIR" ? "not_directory" : code === "EACCES" || code === "EPERM" ? "denied" : "unavailable",
138
+ message: err.message,
139
+ } };
140
+ }
141
+ const out = [];
142
+ for (const d of dirents) {
143
+ if (ignored.has(d.name))
144
+ continue;
145
+ let type;
146
+ if (IS_WIN32 && d.isSymbolicLink()) {
147
+ // mklink /D symlinks and junctions are reparse points — libuv
148
+ // classifies them as links, so isDirectory() is false. Follow the
149
+ // target so folder links stay navigable; broken links still show.
150
+ try {
151
+ const st = await fs.stat(join(abs, d.name));
152
+ type = st.isDirectory() ? "dir" : "file";
153
+ }
154
+ catch {
155
+ type = "file";
156
+ }
157
+ }
158
+ else {
159
+ type = d.isDirectory() ? "dir" : "file";
160
+ }
161
+ const entry = {
162
+ name: d.name,
163
+ path: rel === "" ? d.name : `${rel}/${d.name}`,
164
+ type,
165
+ };
166
+ if (type === "file")
167
+ entry.kind = previewKind(d.name);
168
+ out.push(entry);
169
+ }
170
+ out.sort((a, b) => a.type === b.type
171
+ ? a.name.localeCompare(b.name)
172
+ : a.type === "dir"
173
+ ? -1
174
+ : 1);
175
+ const truncated = out.length > MAX;
176
+ if (truncated)
177
+ out.length = MAX;
178
+ return { entries: out, truncated };
179
+ }
180
+ export class FilesService {
181
+ host;
182
+ // ---- 当前列出目录的 watcher ----
183
+ fsWatcher = null;
184
+ watchPath = null;
185
+ watchTimer = null;
186
+ /** Recursive-watch state (win32/darwin): one native recursive watcher on the
187
+ * workspace root covers the WHOLE tree; watchPath just tracks which listed
188
+ * directory file_changed should refresh. */
189
+ recursiveWatcher = false;
190
+ watchRoot = null;
191
+ /** 已对哪个工作区根提示过「实时监听不可用,已回落轮询」——只提示一次。 */
192
+ degradedNoticedFor = null;
193
+ // ---- git dir watcher ----
194
+ gitWatcher = null;
195
+ gitWatchCwd = null;
196
+ gitDirtyTimer = null;
197
+ constructor(host) {
198
+ this.host = host;
199
+ }
200
+ listingRequests = new Map();
201
+ async listFiles(relPath) {
202
+ const cwd = this.host.getCwd();
203
+ const key = `${cwd}\0${relPath ?? ""}`;
204
+ const existing = this.listingRequests.get(key);
205
+ if (existing)
206
+ return existing;
207
+ const pending = this.scanFiles(cwd, relPath).finally(() => this.listingRequests.delete(key));
208
+ this.listingRequests.set(key, pending);
209
+ return pending;
210
+ }
211
+ async scanFiles(cwd, relPath) {
212
+ const { resolve, sep, relative } = await import("node:path");
213
+ const root = resolve(cwd);
214
+ const target = relPath ? resolve(root, relPath) : root;
215
+ const rawRel = relative(root, target);
216
+ if (rawRel.startsWith("..") || rawRel.includes(`${sep}..`)) {
217
+ this.host.emit({
218
+ type: "notice",
219
+ level: "warning",
220
+ text: `路径超出工作区:${relPath ?? ""}`,
221
+ });
222
+ return;
223
+ }
224
+ // Normalize to forward slashes: the wire protocol and the frontend
225
+ // always use "/", but relative() returns "\\" on Windows.
226
+ const rel = rawRel.split(sep).join("/");
227
+ const { entries, truncated, error } = await readDirForUI(target, rel);
228
+ this.host.emit({
229
+ type: "files",
230
+ cwd,
231
+ path: rel === "" ? "" : rel,
232
+ parent: rel === ""
233
+ ? null
234
+ : rel.includes("/")
235
+ ? rel.slice(0, rel.lastIndexOf("/"))
236
+ : "",
237
+ entries,
238
+ truncated,
239
+ ...(error ? { error } : {}),
240
+ });
241
+ // Watch the listed directory — deferred until AFTER the response is on
242
+ // the wire. On win32, fs.watch(root, {recursive:true}) registering over
243
+ // a big tree (node_modules et al) can itself take a noticeable moment
244
+ // (ReadDirectoryChangesW setup cost scales with tree size, unlike
245
+ // macOS's FSEvents); doing it inline before the emit made every
246
+ // project switch wait on that registration instead of just seeing the
247
+ // file list immediately and having the watcher arm a beat later.
248
+ if (!error && cwd === this.host.getCwd())
249
+ this.watchDir(target, rel);
250
+ }
251
+ /**
252
+ * Global search: recursive filename match across the active workspace.
253
+ * Best-effort bounded walk — ignored dirs (node_modules/.git/…) are
254
+ * skipped, unreadable dirs silently passed, and hard caps on results /
255
+ * visited entries / elapsed time keep big repos responsive. Always answers
256
+ * with a search_files_result echoing reqId so the client's request never
257
+ * stalls (ok:false on unexpected failure).
258
+ */
259
+ async searchFiles(query, reqId) {
260
+ const cwd = this.host.getActiveCwd();
261
+ const { join } = await import("node:path");
262
+ const fsp = await import("node:fs/promises");
263
+ const q = query.trim().toLowerCase();
264
+ if (!q) {
265
+ this.host.emit({ type: "search_files_result", cwd, reqId, ok: true, results: [] });
266
+ return;
267
+ }
268
+ const root = resolve(cwd);
269
+ const ignored = ignoredEntries();
270
+ const MAX_RESULTS = 50;
271
+ const MAX_VISITED = 20000;
272
+ const MAX_MS = 4000;
273
+ const start = Date.now();
274
+ const results = [];
275
+ let visited = 0;
276
+ let truncated = false;
277
+ const budgetLeft = () => results.length < MAX_RESULTS &&
278
+ visited < MAX_VISITED &&
279
+ Date.now() - start < MAX_MS;
280
+ // Breadth-first-ish iterative stack; depth cap is a symlink-cycle guard.
281
+ const walk = async (abs, rel, depth) => {
282
+ if (!budgetLeft() || depth > 24) {
283
+ truncated = true;
284
+ return;
285
+ }
286
+ let dirents;
287
+ try {
288
+ dirents = await fsp.readdir(abs, { withFileTypes: true });
289
+ }
290
+ catch {
291
+ return; // unreadable dir (ACL/permissions) — skip silently
292
+ }
293
+ for (const d of dirents) {
294
+ visited++;
295
+ if (!budgetLeft()) {
296
+ truncated = true;
297
+ break;
298
+ }
299
+ if (ignored.has(d.name))
300
+ continue;
301
+ const childRel = rel ? `${rel}/${d.name}` : d.name;
302
+ if (d.name.toLowerCase().includes(q)) {
303
+ results.push({
304
+ path: childRel,
305
+ name: d.name,
306
+ type: d.isDirectory() ? "dir" : "file",
307
+ });
308
+ }
309
+ if (d.isDirectory()) {
310
+ await walk(join(abs, d.name), childRel, depth + 1);
311
+ }
312
+ }
313
+ };
314
+ try {
315
+ await walk(root, "", 0);
316
+ this.host.emit({
317
+ type: "search_files_result", cwd,
318
+ reqId,
319
+ ok: true,
320
+ results,
321
+ ...(truncated ? { truncated: true } : {}),
322
+ });
323
+ }
324
+ catch {
325
+ this.host.emit({ type: "search_files_result", cwd, reqId, ok: false, results: [] });
326
+ }
327
+ }
328
+ /**
329
+ * Source-control panel: read-only git queries via server-side execFile
330
+ * (no shell, no prompts). Always responds with an scm_data message echoing
331
+ * reqId so the client's request matching never stalls. Also (re)arms the
332
+ * git-dir watcher so external repo changes push scm_changed.
333
+ */
334
+ branchRequests = new Map();
335
+ diffRequests = new Map();
336
+ async gitBranch() {
337
+ const cwd = this.host.getActiveCwd();
338
+ const pending = this.branchRequests.get(cwd);
339
+ if (pending)
340
+ return pending;
341
+ const request = (async () => {
342
+ await this.watchGitDir(cwd);
343
+ const info = await scmCurrentBranch(cwd);
344
+ this.host.emit({ type: "git_branch", cwd, ...info });
345
+ })().finally(() => this.branchRequests.delete(cwd));
346
+ this.branchRequests.set(cwd, request);
347
+ return request;
348
+ }
349
+ async scmQuery(kind, reqId, arg) {
350
+ const cwd = this.host.getActiveCwd();
351
+ if (kind === "status")
352
+ this.watchGitDir(cwd);
353
+ try {
354
+ if (kind === "status") {
355
+ const data = await scmStatus(cwd);
356
+ this.host.emit({ type: "scm_data", cwd, reqId, kind, ok: true, ...data });
357
+ return;
358
+ }
359
+ if (kind === "diff" && (arg?.scope === "branch" || arg?.scope === "work")) {
360
+ // The panel refreshes on a timer and on every file change; join a
361
+ // request that is still running instead of starting another git diff.
362
+ const key = `${cwd}\0${arg.scope}\0${arg.base ?? ""}`;
363
+ let pending = this.diffRequests.get(key);
364
+ if (!pending) {
365
+ pending = scmDiff(cwd, { scope: arg.scope, base: arg.base }).finally(() => this.diffRequests.delete(key));
366
+ this.diffRequests.set(key, pending);
367
+ }
368
+ const data = await pending;
369
+ this.host.emit({ type: "scm_data", cwd, reqId, kind, ok: true, ...data });
370
+ return;
371
+ }
372
+ if (kind === "history") {
373
+ const history = await scmHistory(cwd);
374
+ this.host.emit({ type: "scm_data", cwd, reqId, kind, ok: true, history });
375
+ return;
376
+ }
377
+ if (kind === "filediff" && arg?.path) {
378
+ // Path stays inside the workspace (defense in depth — paths come
379
+ // from our own listing, and execFile passes args verbatim anyway).
380
+ const { resolve, relative } = await import("node:path");
381
+ const rel = relative(resolve(cwd), resolve(cwd, arg.path));
382
+ if (rel.startsWith("..") || rel === "")
383
+ throw new Error("路径超出工作区");
384
+ const diff = await scmFileDiff(cwd, arg.path);
385
+ this.host.emit({
386
+ type: "scm_data", cwd, reqId, kind, ok: true,
387
+ stagedText: diff.staged, worktreeText: diff.worktree,
388
+ untracked: diff.untracked,
389
+ untrackedText: diff.untrackedText,
390
+ untrackedKind: diff.untrackedKind,
391
+ untrackedTruncated: diff.untrackedTruncated,
392
+ });
393
+ return;
394
+ }
395
+ if (kind === "commit" && arg?.hash && /^[0-9a-f]{7,40}$/i.test(arg.hash)) {
396
+ const text = await scmCommitDetail(cwd, arg.hash);
397
+ this.host.emit({ type: "scm_data", cwd, reqId, kind, ok: true, text });
398
+ return;
399
+ }
400
+ throw new Error("无效的 scm 查询参数");
401
+ }
402
+ catch (err) {
403
+ if (isNotRepoError(err)) {
404
+ // Not a repo — a valid empty answer so the panel shows its hint.
405
+ this.host.emit({
406
+ type: "scm_data", cwd, reqId, kind, ok: true, notRepo: true,
407
+ branch: "", detached: false, upstream: null,
408
+ ahead: 0, behind: 0, upstreamGone: false,
409
+ files: [], branches: [], stats: {}, history: [],
410
+ });
411
+ this.unwatchGit();
412
+ return;
413
+ }
414
+ this.host.emit({
415
+ type: "scm_data", cwd,
416
+ reqId,
417
+ kind,
418
+ ok: false,
419
+ error: err instanceof Error ? err.message : String(err),
420
+ });
421
+ }
422
+ }
423
+ /**
424
+ * Watch the active repo's git dir (HEAD / index / packed-refs live at the
425
+ * top level, which covers commit / stage / checkout). Re-targets when the
426
+ * queried workspace changes. Uses `git rev-parse --absolute-git-dir` so
427
+ * worktrees and submodules resolve to the real dir.
428
+ */
429
+ gitWatchGeneration = 0;
430
+ async watchGitDir(cwd) {
431
+ if (this.gitWatchCwd === cwd && this.gitWatcher)
432
+ return;
433
+ this.unwatchGit();
434
+ this.gitWatchCwd = cwd;
435
+ const generation = this.gitWatchGeneration;
436
+ try {
437
+ const gitDir = await gitDirOf(cwd);
438
+ if (!gitDir || this.host.isDisposed() || this.gitWatchGeneration !== generation)
439
+ return;
440
+ this.gitWatcher = watch(gitDir, { persistent: false }, () => {
441
+ if (this.host.isDisposed() || this.gitDirtyTimer)
442
+ return;
443
+ // Debounce: one checkout/commit fires several fs events.
444
+ this.gitDirtyTimer = setTimeout(() => {
445
+ this.gitDirtyTimer = null;
446
+ this.host.emit({ type: "scm_changed" });
447
+ }, 600);
448
+ });
449
+ this.gitWatcher.on("error", () => {
450
+ // Unsupported filesystem — silently fall back to manual refresh.
451
+ this.unwatchGit();
452
+ });
453
+ }
454
+ catch {
455
+ // no .git here (or git missing) — watcher stays off; queries still work
456
+ if (this.gitWatchGeneration === generation)
457
+ this.unwatchGit();
458
+ }
459
+ }
460
+ unwatchGit() {
461
+ this.gitWatchGeneration++;
462
+ if (this.gitWatcher) {
463
+ try {
464
+ this.gitWatcher.close();
465
+ }
466
+ catch {
467
+ // already gone
468
+ }
469
+ }
470
+ this.gitWatcher = null;
471
+ this.gitWatchCwd = null;
472
+ if (this.gitDirtyTimer) {
473
+ clearTimeout(this.gitDirtyTimer);
474
+ this.gitDirtyTimer = null;
475
+ }
476
+ }
477
+ watchDir(absPath, rel) {
478
+ if (this.host.isDisposed())
479
+ return;
480
+ // ---- Recursive mode (native on win32 / darwin): watch the workspace
481
+ // root once, so deep changes in NOT-listed subdirectories still refresh
482
+ // the panel (the old per-directory watch only saw the current level).
483
+ if (process.platform === "win32" || process.platform === "darwin") {
484
+ const root = resolve(this.host.getCwd());
485
+ if (this.recursiveWatcher) {
486
+ if (this.watchRoot === root) {
487
+ // Same workspace — only retarget the refresh path.
488
+ this.watchPath = rel;
489
+ return;
490
+ }
491
+ this.unwatchDir(); // cwd switched to another project
492
+ }
493
+ try {
494
+ const w = watch(root, { persistent: false, recursive: true }, (_event, filename) => {
495
+ // Skip high-churn subtrees (npm install storms); .git has its
496
+ // own watcher for the SCM panel. filename may be null on some
497
+ // platforms — let those through (debounce absorbs bursts).
498
+ if (filename) {
499
+ const f = String(filename).split("\\").join("/");
500
+ // Single-segment names (e.g. the dir itself) have no "/" —
501
+ // slice(0, -1) would corrupt them, so special-case that.
502
+ const slash = f.indexOf("/");
503
+ const top = slash === -1 ? f : f.slice(0, slash);
504
+ if (top === "node_modules" || top === ".git")
505
+ return;
506
+ }
507
+ // Burst events are debounced into a single refresh.
508
+ if (this.watchTimer)
509
+ return;
510
+ this.watchTimer = setTimeout(() => {
511
+ this.watchTimer = null;
512
+ this.host.emit({
513
+ type: "file_changed",
514
+ path: this.watchPath ?? "",
515
+ });
516
+ }, 400);
517
+ });
518
+ w.on("error", () => {
519
+ // Directory deleted / unsupported — fall back to poll semantics.
520
+ this.noticeDegraded(root);
521
+ this.unwatchDir();
522
+ });
523
+ this.fsWatcher = w;
524
+ this.watchRoot = root;
525
+ this.recursiveWatcher = true;
526
+ this.watchPath = rel;
527
+ return;
528
+ }
529
+ catch {
530
+ // recursive unsupported here — fall through to per-directory watch.
531
+ this.fsWatcher = null;
532
+ this.recursiveWatcher = false;
533
+ this.watchRoot = null;
534
+ this.noticeDegraded(root);
535
+ }
536
+ }
537
+ // ---- Fallback: single non-recursive watch on the LISTED directory.
538
+ if (!this.recursiveWatcher && this.watchPath === rel && this.fsWatcher)
539
+ return;
540
+ this.unwatchDir();
541
+ this.watchPath = rel;
542
+ try {
543
+ // persistent: false — the watcher must not keep the process alive.
544
+ this.fsWatcher = watch(absPath, { persistent: false }, () => {
545
+ // Burst events (npm install, git ops, editor save→rename) are
546
+ // debounced into a single refresh.
547
+ if (this.watchTimer)
548
+ return;
549
+ this.watchTimer = setTimeout(() => {
550
+ this.watchTimer = null;
551
+ this.host.emit({ type: "file_changed", path: this.watchPath ?? "" });
552
+ }, 400);
553
+ });
554
+ this.fsWatcher.on("error", () => {
555
+ // Directory deleted / unsupported fs — stop watching; the poll (or
556
+ // the next navigation) restores things.
557
+ this.unwatchDir();
558
+ });
559
+ }
560
+ catch {
561
+ // fs.watch unsupported (some network mounts, containers) — poll covers it.
562
+ this.fsWatcher = null;
563
+ this.watchPath = null;
564
+ this.noticeDegraded(absPath);
565
+ }
566
+ }
567
+ /** 实时监听不可用(网络盘/WSL/受限目录):一次性告知用户已回落 10s 轮询,
568
+ * 免得疑惑「面板为什么不实时」。每个工作区根只提示一次。 */
569
+ noticeDegraded(root) {
570
+ if (this.degradedNoticedFor === root)
571
+ return;
572
+ this.degradedNoticedFor = root;
573
+ this.host.emit({
574
+ type: "notice",
575
+ level: "info",
576
+ text: "此目录不支持实时文件监听(网络盘/受限目录),文件面板已改为每 10 秒自动刷新。",
577
+ });
578
+ }
579
+ unwatchDir() {
580
+ if (this.watchTimer) {
581
+ clearTimeout(this.watchTimer);
582
+ this.watchTimer = null;
583
+ }
584
+ if (this.fsWatcher) {
585
+ try {
586
+ this.fsWatcher.close();
587
+ }
588
+ catch {
589
+ // already closed
590
+ }
591
+ this.fsWatcher = null;
592
+ }
593
+ this.recursiveWatcher = false;
594
+ this.watchRoot = null;
595
+ this.watchPath = null;
596
+ }
597
+ /** Read a workspace file for the preview panel (size-capped, binary-safe). */
598
+ async readFile(relPath, options = {}) {
599
+ const cwd = this.host.getCwd();
600
+ try {
601
+ if (options.cwd !== undefined && options.cwd !== cwd)
602
+ throw new Error("工作区已改变");
603
+ const fs = await import("node:fs/promises");
604
+ const root = cwd;
605
+ const wp = workspacePath(resolve(root), relPath);
606
+ if (!wp) {
607
+ throw new Error("路径超出工作区");
608
+ }
609
+ const { abs, rel } = wp;
610
+ if (!isRealPathInWorkspace(root, abs))
611
+ throw new Error("路径超出工作区");
612
+ const stat = await fs.stat(abs);
613
+ if (!stat.isFile()) {
614
+ throw new Error("不是文件");
615
+ }
616
+ const name = relPath.split(/[\\/]/).pop() ?? relPath;
617
+ const kind = previewKind(name);
618
+ // Media previews stream over the /api/file HTTP endpoint, so only
619
+ // metadata is sent here — the raw bytes never touch the socket.
620
+ if (kind === "image" || kind === "video" || kind === "sqlite") {
621
+ this.host.emit({
622
+ type: "file_content",
623
+ requestId: options.requestId,
624
+ cwd,
625
+ path: rel,
626
+ name,
627
+ text: "",
628
+ truncated: false,
629
+ binary: true,
630
+ kind,
631
+ lines: 0,
632
+ size: stat.size,
633
+ });
634
+ return;
635
+ }
636
+ // Everything else: read a capped prefix and sniff the content.
637
+ // Anything that looks like text previews as text regardless of its
638
+ // extension (jsonl, .log.1, weird suffixes, …); binary content gets
639
+ // a hex dump of the first few KB instead of being refused.
640
+ const handle = await fs.open(abs, "r");
641
+ try {
642
+ const buf = Buffer.alloc(Math.min(stat.size, MAX_PREVIEW_BYTES));
643
+ const { bytesRead } = await handle.read(buf, 0, buf.length, 0);
644
+ const data = buf.subarray(0, bytesRead);
645
+ if (data.subarray(0, 16).toString("utf8") === "SQLite format 3\0") {
646
+ this.host.emit({ type: "file_content", requestId: options.requestId, cwd, path: rel, name, text: "", truncated: false, binary: true, kind: "sqlite", lines: 0, size: stat.size });
647
+ }
648
+ else if (looksLikeText(data)) {
649
+ this.host.emit({
650
+ type: "file_content",
651
+ requestId: options.requestId,
652
+ cwd,
653
+ path: rel,
654
+ name,
655
+ text: decodeText(data),
656
+ version: createHash("sha256").update(data).digest("hex"),
657
+ truncated: bytesRead < stat.size,
658
+ binary: false,
659
+ kind: "text",
660
+ lines: countLines(data),
661
+ size: stat.size,
662
+ });
663
+ }
664
+ else {
665
+ this.host.emit({
666
+ type: "file_content",
667
+ requestId: options.requestId,
668
+ cwd,
669
+ path: rel,
670
+ name,
671
+ text: hexDump(data),
672
+ truncated: bytesRead < stat.size,
673
+ binary: true,
674
+ kind: kind === "text" ? "text" : "none",
675
+ lines: 0,
676
+ size: stat.size,
677
+ });
678
+ }
679
+ }
680
+ finally {
681
+ await handle.close();
682
+ }
683
+ }
684
+ catch (err) {
685
+ this.host.emit({ type: "file_result", operation: "read", requestId: options.requestId, cwd, path: relPath, ok: false, error: err.message });
686
+ }
687
+ }
688
+ /** Save text from the file preview panel within the active workspace. */
689
+ async writeFile(relPath, text, options = {}) {
690
+ const cwd = this.host.getCwd();
691
+ const result = { type: "file_result", operation: "write", requestId: options.requestId, cwd, path: relPath };
692
+ try {
693
+ if (options.cwd !== undefined && options.cwd !== cwd)
694
+ throw new Error("工作区已改变");
695
+ const wp = workspacePath(resolve(cwd), relPath);
696
+ if (!wp || !isRealPathInWorkspace(cwd, wp.abs))
697
+ throw new Error("路径超出工作区");
698
+ if (Buffer.byteLength(text, "utf8") > 2 * 1024 * 1024)
699
+ throw new Error("文件内容过大(上限 2MB)");
700
+ const stat = statSync(wp.abs);
701
+ if (!stat.isFile() || stat.size > MAX_PREVIEW_BYTES)
702
+ throw new Error("文件不是可编辑的完整文本");
703
+ const data = readFileSync(wp.abs);
704
+ if (!looksLikeText(data))
705
+ throw new Error("二进制文件不可编辑");
706
+ const version = createHash("sha256").update(data).digest("hex");
707
+ if (!options.force && options.expectedVersion !== version) {
708
+ this.host.emit({ ...result, ok: false, conflict: true, error: "磁盘内容已改变,请重新加载或明确覆盖" });
709
+ return;
710
+ }
711
+ // No await between checking the version and writing: concurrent UI saves serialize.
712
+ writeFileSync(wp.abs, text, "utf8");
713
+ this.host.emit({ ...result, ok: true, version: createHash("sha256").update(text).digest("hex") });
714
+ }
715
+ catch (err) {
716
+ this.host.emit({ ...result, ok: false, error: err.message });
717
+ }
718
+ }
719
+ /**
720
+ * Path completion for the cwd input: expand ~/relative paths, list the parent
721
+ * directory, and return prefix matches (dirs first, capped).
722
+ */
723
+ async completePath(input) {
724
+ const empty = () => this.host.emit({ type: "path_completions", completions: [] });
725
+ try {
726
+ const fs = await import("node:fs/promises");
727
+ const { resolve, sep, isAbsolute } = await import("node:path");
728
+ const { homedir } = await import("node:os");
729
+ const home = homedir();
730
+ const cwd = this.host.getCwd();
731
+ // Expand ~ and relative inputs to an absolute path. Windows users type
732
+ // backslashes (P:\agent) and ~\ — handle both separator styles.
733
+ let expanded = input.trim();
734
+ if (expanded === "") {
735
+ empty();
736
+ return;
737
+ }
738
+ if (expanded === "~" || expanded === "~\\") {
739
+ expanded = home;
740
+ }
741
+ else if (expanded.startsWith("~/") || expanded.startsWith("~\\")) {
742
+ expanded = home + sep + expanded.slice(2);
743
+ }
744
+ else if (!isAbsolute(expanded)) {
745
+ expanded = resolve(cwd, expanded);
746
+ }
747
+ // Split into parent dir + prefix on the LAST separator of either style
748
+ // (Windows accepts both / and \, so P:\agent/de must work too).
749
+ const lastSlash = Math.max(expanded.lastIndexOf("/"), expanded.lastIndexOf("\\"));
750
+ const dirPart = lastSlash >= 0 ? expanded.slice(0, lastSlash + 1) : "";
751
+ const prefix = lastSlash >= 0 ? expanded.slice(lastSlash + 1) : expanded;
752
+ const dirents = await fs
753
+ .readdir(dirPart, { withFileTypes: true })
754
+ .catch(() => null);
755
+ if (!dirents) {
756
+ empty();
757
+ return;
758
+ }
759
+ const { join } = await import("node:path");
760
+ const completions = dirents
761
+ .filter((d) => d.name.startsWith(prefix) && !ignoredEntries().has(d.name))
762
+ .map((d) => ({
763
+ name: d.name,
764
+ // Windows users type backslashes — normalize the completion to the
765
+ // wire format ("/") so the picked path round-trips cleanly.
766
+ path: IS_WIN32
767
+ ? join(dirPart, d.name).split(sep).join("/")
768
+ : dirPart + d.name,
769
+ type: (d.isDirectory() ? "dir" : "file"),
770
+ }))
771
+ .sort((a, b) => {
772
+ const aHidden = a.name.startsWith(".");
773
+ const bHidden = b.name.startsWith(".");
774
+ if (aHidden !== bHidden)
775
+ return aHidden ? 1 : -1;
776
+ if (a.type !== b.type)
777
+ return a.type === "dir" ? -1 : 1;
778
+ return a.name.localeCompare(b.name);
779
+ })
780
+ .slice(0, 30);
781
+ this.host.emit({ type: "path_completions", completions });
782
+ }
783
+ catch {
784
+ empty();
785
+ }
786
+ }
787
+ }