@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.
- package/LICENSE +21 -0
- package/README.md +74 -2
- package/bin/pi-harness.mjs +1819 -0
- package/deploy/com.youweichen.pi-harness.plist +48 -0
- package/deploy/nginx-subpath.conf +88 -0
- package/deploy/pi-harness-task.xml +71 -0
- package/deploy/pi-harness.service +31 -0
- package/dist/server/agent-service.js +3131 -0
- package/dist/server/app-version.js +19 -0
- package/dist/server/attachments.js +526 -0
- package/dist/server/auth-file.js +61 -0
- package/dist/server/bg-servers.js +154 -0
- package/dist/server/client-eviction.js +19 -0
- package/dist/server/client-id.js +9 -0
- package/dist/server/client-state.js +202 -0
- package/dist/server/codemode-image-routes.js +36 -0
- package/dist/server/codemode-presentation.js +19 -0
- package/dist/server/component-updates.js +135 -0
- package/dist/server/context-breakdown.js +56 -0
- package/dist/server/control-socket.js +178 -0
- package/dist/server/conversation.js +65 -0
- package/dist/server/dir-browser.js +77 -0
- package/dist/server/editor-snapshot.js +38 -0
- package/dist/server/ensure-bash.js +80 -0
- package/dist/server/extension-display.js +23 -0
- package/dist/server/extensions-catalog.js +64 -0
- package/dist/server/extensions-model.js +46 -0
- package/dist/server/extensions-routes.js +255 -0
- package/dist/server/extensions-worker.js +188 -0
- package/dist/server/files-service.js +787 -0
- package/dist/server/index.js +1033 -0
- package/dist/server/markdown-images.js +42 -0
- package/dist/server/model-admin.js +801 -0
- package/dist/server/model-config-merge.js +32 -0
- package/dist/server/native-mcp-config.js +103 -0
- package/dist/server/native-mcp-presentation.js +12 -0
- package/dist/server/native-prompt-template.js +12 -0
- package/dist/server/native-tools.js +32 -0
- package/dist/server/node-agent.js +262 -0
- package/dist/server/node-sources.js +142 -0
- package/dist/server/node-workbench.js +729 -0
- package/dist/server/origin-policy.js +50 -0
- package/dist/server/patch-node-pty.js +96 -0
- package/dist/server/pi-environment.js +203 -0
- package/dist/server/plan/extension.js +74 -0
- package/dist/server/plan/progress.js +90 -0
- package/dist/server/plan/settings.js +41 -0
- package/dist/server/plan/state.js +123 -0
- package/dist/server/plugin-facilities.js +304 -0
- package/dist/server/plugin-updater.js +245 -0
- package/dist/server/plugins.js +712 -0
- package/dist/server/private-file.js +28 -0
- package/dist/server/process-utils.js +112 -0
- package/dist/server/prompt-delivery.js +80 -0
- package/dist/server/protocol-version.js +11 -0
- package/dist/server/protocol.js +6 -0
- package/dist/server/provider-auth.js +99 -0
- package/dist/server/query-cache.js +45 -0
- package/dist/server/recovery-state.js +37 -0
- package/dist/server/scm.js +459 -0
- package/dist/server/security-headers.js +24 -0
- package/dist/server/serialize.js +254 -0
- package/dist/server/session-file-read.js +225 -0
- package/dist/server/session-record-worker.js +21 -0
- package/dist/server/session-reload.js +13 -0
- package/dist/server/session-tree-controller.js +250 -0
- package/dist/server/session-tree.js +191 -0
- package/dist/server/settings-service.js +53 -0
- package/dist/server/skills-service.js +87 -0
- package/dist/server/slash-commands.js +286 -0
- package/dist/server/sqlite-preview.js +58 -0
- package/dist/server/sqlite-query.js +67 -0
- package/dist/server/sqlite-worker.js +12 -0
- package/dist/server/system-prompt-files.js +158 -0
- package/dist/server/system-prompt-routes.js +40 -0
- package/dist/server/system-prompt-view.js +49 -0
- package/dist/server/task-progress.js +233 -0
- package/dist/server/terminals.js +833 -0
- package/dist/server/text-sniff.js +269 -0
- package/dist/server/thinking-timing.js +91 -0
- package/dist/server/token-auth.js +37 -0
- package/dist/server/tool-output.js +97 -0
- package/dist/server/tool-text-incidents.js +82 -0
- package/dist/server/tool-text.js +67 -0
- package/dist/server/uploads.js +112 -0
- package/dist/server/user-attachments.js +96 -0
- package/dist/server/webui-context.js +257 -0
- package/dist/server/wiki-links.js +136 -0
- package/dist/server/wiki-pdf-worker.js +23 -0
- package/dist/server/wiki-pdf.js +36 -0
- package/dist/server/wiki-routes.js +184 -0
- package/dist/server/wiki-service.js +524 -0
- package/electron/main.mjs +607 -0
- package/extensions/webui.ts +192 -0
- package/package.json +122 -4
- package/web/dist/apple-touch-icon.png +0 -0
- package/web/dist/assets/NodeWorkbench-BiP2ktQP.js +3 -0
- package/web/dist/assets/TerminalPanel-CKaSUsN-.js +2 -0
- package/web/dist/assets/abnfDiagram-VCTEODGH-CnwRJDRL.js +1 -0
- package/web/dist/assets/arc-CG4Q7-W_.js +1 -0
- package/web/dist/assets/architectureDiagram-5GKGNRK7-BrGqX_Rg.js +36 -0
- package/web/dist/assets/blockDiagram-I7D4REHJ-C2t0PjZT.js +129 -0
- package/web/dist/assets/c4Diagram-7LVT6UL2-CadrcgGL.js +38 -0
- package/web/dist/assets/channel-C27Jizb6.js +1 -0
- package/web/dist/assets/chunk-2Q5K7J3B-CC3V2ITZ.js +1 -0
- package/web/dist/assets/chunk-5VM5RSS4-DqKSbGmE.js +15 -0
- package/web/dist/assets/chunk-F27PBJKO-_CDr3JbY.js +1 -0
- package/web/dist/assets/chunk-IMKFNOWR-BIR4H4fX.js +231 -0
- package/web/dist/assets/chunk-JWPE2WC7-CrsdBGXT.js +1 -0
- package/web/dist/assets/chunk-POPQ4Y6H-C6q9kQmf.js +1 -0
- package/web/dist/assets/chunk-SVP7TREG-l2XXTr2v.js +88 -0
- package/web/dist/assets/chunk-TICWLB2K-29I_mZgB.js +206 -0
- package/web/dist/assets/chunk-XXDRQBXY-htV9FBqi.js +1 -0
- package/web/dist/assets/classDiagram-ZZMXUADV-DYTBrAEP.js +1 -0
- package/web/dist/assets/classDiagram-v2-VYDZK3BY-DYTBrAEP.js +1 -0
- package/web/dist/assets/cose-bilkent-JH36ORCC-Cop-ihyx.js +1 -0
- package/web/dist/assets/cynefin-OW5HDTMX-IEk8qBRq.js +178 -0
- package/web/dist/assets/cynefinDiagram-5FMLGOSQ-Bmzt6Qb5.js +62 -0
- package/web/dist/assets/cytoscape.esm-Bch-eiPH.js +331 -0
- package/web/dist/assets/dagre-GXQ25YYZ-DiXTJYYU.js +4 -0
- package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/web/dist/assets/diagram-S7CK7UJ4-BT7CLuNk.js +30 -0
- package/web/dist/assets/diagram-UQ7AKVKN-BZZ3nTCO.js +41 -0
- package/web/dist/assets/diagram-VSXAHHWV-Dal8LeZh.js +3 -0
- package/web/dist/assets/diagram-VX7I27RA-D6V9FfNA.js +24 -0
- package/web/dist/assets/diagram-Z3DM3KII-KUAIcFiD.js +24 -0
- package/web/dist/assets/ebnfDiagram-PWID7BFC-s0FEPZrV.js +1 -0
- package/web/dist/assets/erDiagram-RLTQ6QDP-CiIQPFCD.js +99 -0
- package/web/dist/assets/flowDiagram-HODETNUW-CvG4MYWn.js +168 -0
- package/web/dist/assets/ganttDiagram-EL5Y4UJY-CUf115ii.js +292 -0
- package/web/dist/assets/gitGraphDiagram-WWUBYQGX-o3K1r6fL.js +106 -0
- package/web/dist/assets/index-C3vDFSEV.css +10 -0
- package/web/dist/assets/index-C5or_GAq.js +276 -0
- package/web/dist/assets/infoDiagram-27XIBGKW-BxiBeQ8E.js +2 -0
- package/web/dist/assets/init-Gi6I4Gst.js +1 -0
- package/web/dist/assets/ishikawaDiagram-5VMMS53U-B1ONuCkH.js +70 -0
- package/web/dist/assets/journeyDiagram-3NMN7TZE-DwmuoXcX.js +139 -0
- package/web/dist/assets/kanban-definition-UXKFOSKX-pMKHubxH.js +89 -0
- package/web/dist/assets/katex-C5jXJg4s.js +257 -0
- package/web/dist/assets/layout-CEwfjQR0.js +1 -0
- package/web/dist/assets/linear-D22Gdbt1.js +1 -0
- package/web/dist/assets/markdown-BhJ_LInD.js +51 -0
- package/web/dist/assets/mermaid.core-CO2nrD04.js +312 -0
- package/web/dist/assets/mindmap-definition-YA3MSWOX-9MQn9ZAp.js +96 -0
- package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/web/dist/assets/pegDiagram-XKGWAZYB-Dy6fLjM-.js +1 -0
- package/web/dist/assets/pieDiagram-E7YTZNPT-EDLez6nA.js +39 -0
- package/web/dist/assets/quadrantDiagram-AXDQQJYC-Cgcd9mvy.js +7 -0
- package/web/dist/assets/railroadDiagram-O6MQD6OU-B4Td0tjj.js +1 -0
- package/web/dist/assets/react-BddUxV-9.js +24 -0
- package/web/dist/assets/requirementDiagram-BXWQKSXE-aF2QNZ78.js +84 -0
- package/web/dist/assets/sankeyDiagram-P5KCCOFB-DgTYwYMa.js +40 -0
- package/web/dist/assets/sequenceDiagram-WJ2MYXX4-EUcddWUc.js +162 -0
- package/web/dist/assets/sizeCapture-INFHLROL-CXHOv9Sx.js +1 -0
- package/web/dist/assets/stateDiagram-D77RDMKH-B-xlgfaK.js +1 -0
- package/web/dist/assets/stateDiagram-v2-MP3YSRHH-kwAwAlWX.js +1 -0
- package/web/dist/assets/swimlanes-42K2YHIH-BtFBOCDh.js +1 -0
- package/web/dist/assets/swimlanesDiagram-VR7AAH4N-D5j5Cj3S.js +8 -0
- package/web/dist/assets/timeline-definition-24CTP7MA-CZlQY6pb.js +120 -0
- package/web/dist/assets/vennDiagram-4TSXK5OY-Cn61dJvL.js +34 -0
- package/web/dist/assets/wardleyDiagram-VM6X3IG4-fiMNrWpr.js +78 -0
- package/web/dist/assets/xterm-6GBZ9nXN.css +32 -0
- package/web/dist/assets/xterm-D1D2FVe3.js +38 -0
- package/web/dist/assets/xychartDiagram-S5SC5T6Z-CuzM_sof.js +7 -0
- package/web/dist/brand-mark.svg +5 -0
- package/web/dist/favicon-32.png +0 -0
- package/web/dist/favicon.svg +5 -0
- package/web/dist/fonts/WIKI-FONTS.md +5 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-600-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-sans/LICENSE +93 -0
- package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-400-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-500-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-600-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
- package/web/dist/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-400-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-500-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-600-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-700-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-800-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-400-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-500-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-600-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-700-normal.woff2 +0 -0
- package/web/dist/fonts/inter/inter-latin-ext-800-normal.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono/LICENSE +93 -0
- package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-600-normal.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
- package/web/dist/fonts/jetbrains-mono/jetbrains-mono-latin-ext-600-normal.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/LICENSE +94 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-0.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-1.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-10.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-100.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-11.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-12.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-13.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-14.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-15.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-16.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-17.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-18.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-19.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-2.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-20.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-21.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-22.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-23.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-24.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-25.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-26.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-27.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-28.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-29.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-3.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-30.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-31.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-32.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-33.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-34.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-35.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-36.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-37.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-38.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-39.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-4.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-40.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-41.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-42.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-43.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-44.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-45.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-46.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-47.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-48.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-49.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-5.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-50.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-51.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-52.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-53.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-54.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-55.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-56.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-57.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-58.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-59.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-6.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-60.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-61.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-62.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-63.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-64.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-65.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-66.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-67.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-68.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-69.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-7.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-70.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-71.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-72.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-73.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-74.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-75.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-76.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-77.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-78.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-79.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-8.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-80.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-81.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-82.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-83.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-84.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-85.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-86.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-87.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-88.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-89.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-9.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-90.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-91.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-92.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-93.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-94.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-95.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-96.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-97.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-98.woff2 +0 -0
- package/web/dist/fonts/noto-serif-sc/noto-serif-sc-500-99.woff2 +0 -0
- package/web/dist/fonts/source-serif-4/LICENSE +93 -0
- package/web/dist/fonts/source-serif-4/source-serif-4-500-0.woff2 +0 -0
- package/web/dist/fonts/source-serif-4/source-serif-4-500-1.woff2 +0 -0
- package/web/dist/fonts/source-serif-4/source-serif-4-500-2.woff2 +0 -0
- package/web/dist/fonts/source-serif-4/source-serif-4-500-3.woff2 +0 -0
- package/web/dist/fonts/source-serif-4/source-serif-4-500-4.woff2 +0 -0
- package/web/dist/fonts/source-serif-4/source-serif-4-500-5.woff2 +0 -0
- package/web/dist/icon/1a-mark.svg +1 -0
- package/web/dist/icon.ico +0 -0
- package/web/dist/index.html +18 -0
- package/web/public/apple-touch-icon.png +0 -0
- package/web/public/brand-mark.svg +5 -0
- package/web/public/favicon-32.png +0 -0
- package/web/public/favicon.svg +5 -0
- package/web/public/fonts/WIKI-FONTS.md +5 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-mono/ibm-plex-mono-latin-ext-600-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-sans/LICENSE +93 -0
- package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-400-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-500-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-600-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-400-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-500-normal.woff2 +0 -0
- package/web/public/fonts/ibm-plex-sans/ibm-plex-sans-latin-ext-600-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-400-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-500-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-600-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-700-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-800-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-400-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-500-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-600-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-700-normal.woff2 +0 -0
- package/web/public/fonts/inter/inter-latin-ext-800-normal.woff2 +0 -0
- package/web/public/fonts/jetbrains-mono/LICENSE +93 -0
- package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-600-normal.woff2 +0 -0
- package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
- package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
- package/web/public/fonts/jetbrains-mono/jetbrains-mono-latin-ext-600-normal.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/LICENSE +94 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-0.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-1.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-10.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-100.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-11.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-12.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-13.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-14.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-15.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-16.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-17.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-18.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-19.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-2.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-20.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-21.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-22.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-23.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-24.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-25.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-26.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-27.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-28.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-29.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-3.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-30.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-31.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-32.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-33.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-34.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-35.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-36.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-37.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-38.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-39.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-4.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-40.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-41.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-42.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-43.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-44.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-45.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-46.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-47.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-48.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-49.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-5.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-50.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-51.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-52.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-53.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-54.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-55.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-56.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-57.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-58.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-59.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-6.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-60.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-61.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-62.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-63.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-64.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-65.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-66.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-67.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-68.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-69.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-7.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-70.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-71.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-72.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-73.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-74.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-75.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-76.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-77.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-78.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-79.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-8.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-80.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-81.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-82.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-83.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-84.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-85.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-86.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-87.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-88.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-89.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-9.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-90.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-91.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-92.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-93.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-94.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-95.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-96.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-97.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-98.woff2 +0 -0
- package/web/public/fonts/noto-serif-sc/noto-serif-sc-500-99.woff2 +0 -0
- package/web/public/fonts/source-serif-4/LICENSE +93 -0
- package/web/public/fonts/source-serif-4/source-serif-4-500-0.woff2 +0 -0
- package/web/public/fonts/source-serif-4/source-serif-4-500-1.woff2 +0 -0
- package/web/public/fonts/source-serif-4/source-serif-4-500-2.woff2 +0 -0
- package/web/public/fonts/source-serif-4/source-serif-4-500-3.woff2 +0 -0
- package/web/public/fonts/source-serif-4/source-serif-4-500-4.woff2 +0 -0
- package/web/public/fonts/source-serif-4/source-serif-4-500-5.woff2 +0 -0
- package/web/public/icon/1a-mark.svg +1 -0
- 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
|
+
}
|