@rexvane/piweb 0.3.21 → 0.3.23
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +9 -6
- package/.next/build-manifest.json +5 -5
- package/.next/next-minimal-server.js.nft.json +1 -1
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +7 -7
- package/.next/react-loadable-manifest.json +18 -11
- package/.next/required-server-files.js +16 -1
- package/.next/required-server-files.json +16 -1
- package/.next/routes-manifest.json +20 -0
- package/.next/server/app/_global-error/page.js +2 -2
- package/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.meta +8 -0
- package/.next/server/app/_global-error.rsc +15 -11
- package/.next/server/app/_global-error.segments/_full.segment.rsc +15 -11
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +20 -16
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +3 -1
- package/.next/server/app/_not-found/page.js +1 -1
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.meta +8 -0
- package/.next/server/app/_not-found.rsc +17 -13
- package/.next/server/app/_not-found.segments/_full.segment.rsc +17 -13
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +24 -20
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +4 -2
- package/.next/server/app/api/agent/[id]/events/route.js +2 -2
- package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/[id]/route.js +1 -1
- package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/[id]/side-chat/route.js +5 -0
- package/.next/server/app/api/agent/[id]/side-chat/route.js.nft.json +1 -0
- package/.next/server/app/api/agent/[id]/side-chat/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
- package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/new/route.js +1 -1
- package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
- package/.next/server/app/api/config/route.js +1 -1
- package/.next/server/app/api/config/route.js.nft.json +1 -1
- package/.next/server/app/api/files/route.js +1 -1
- package/.next/server/app/api/files/route.js.nft.json +1 -1
- package/.next/server/app/api/fs/route.js +1 -1
- package/.next/server/app/api/fs/route.js.nft.json +1 -1
- package/.next/server/app/api/git/route.js +4 -4
- package/.next/server/app/api/git/route.js.nft.json +1 -1
- package/.next/server/app/api/github/route.js +5 -0
- package/.next/server/app/api/github/route.js.nft.json +1 -0
- package/.next/server/app/api/github/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/growth/route.js +1 -1
- package/.next/server/app/api/growth/route.js.nft.json +1 -1
- package/.next/server/app/api/health/route.js +1 -1
- package/.next/server/app/api/health/route.js.nft.json +1 -1
- package/.next/server/app/api/models/defaults/route.js +1 -0
- package/.next/server/app/api/models/defaults/route.js.nft.json +1 -0
- package/.next/server/app/api/models/defaults/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/models/route.js +1 -1
- package/.next/server/app/api/models/route.js.nft.json +1 -1
- package/.next/server/app/api/pi-settings/route.js +1 -1
- package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
- package/.next/server/app/api/plugins/route.js +1 -1
- package/.next/server/app/api/plugins/route.js.nft.json +1 -1
- package/.next/server/app/api/prompts/route.js +1 -1
- package/.next/server/app/api/prompts/route.js.nft.json +1 -1
- package/.next/server/app/api/remote/route.js +4 -4
- package/.next/server/app/api/remote/route.js.nft.json +1 -1
- package/.next/server/app/api/security/route.js +1 -1
- package/.next/server/app/api/security/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/import/route.js +2 -2
- package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/route.js +2 -2
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/update/route.js +2 -2
- package/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/server/app/api/version/route.js +1 -1
- package/.next/server/app/api/version/route.js.nft.json +1 -1
- package/.next/server/app/api/web-auth/route.js +2 -2
- package/.next/server/app/api/web-auth/route.js.nft.json +1 -1
- package/.next/server/app/api/workspaces/route.js +1 -1
- package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
- package/.next/server/app/icon.svg/route.js +1 -1
- package/.next/server/app/icon.svg/route.js.nft.json +1 -1
- package/.next/server/app/icon.svg.meta +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.meta +8 -0
- package/.next/server/app/index.rsc +20 -13
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +26 -19
- package/.next/server/app/index.segments/_full.segment.rsc +20 -13
- package/.next/server/app/index.segments/_tree.segment.rsc +4 -2
- package/.next/server/app/login/page.js +1 -1
- package/.next/server/app/login/page.js.nft.json +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +1 -1
- package/.next/server/app/login.meta +8 -0
- package/.next/server/app/login.rsc +23 -17
- package/.next/server/app/login.segments/_full.segment.rsc +23 -17
- package/.next/server/app/login.segments/_tree.segment.rsc +4 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +30 -24
- package/.next/server/app/page.js +1 -1
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +9 -6
- package/.next/server/chunks/1813.js +23 -0
- package/.next/server/chunks/2430.js +377 -0
- package/.next/server/chunks/2970.js +305 -0
- package/.next/server/chunks/3421.js +1 -0
- package/.next/server/chunks/3924.js +6 -0
- package/.next/server/chunks/4807.js +1 -0
- package/.next/server/chunks/538.js +1 -1
- package/.next/server/chunks/5393.js +1 -0
- package/.next/server/chunks/5802.js +2 -0
- package/.next/server/chunks/6229.js +1 -0
- package/.next/server/chunks/7496.js +1 -0
- package/.next/server/chunks/7662.js +2 -0
- package/.next/server/chunks/8686.js +1 -0
- package/.next/server/chunks/{593.js → 974.js} +1 -1
- package/.next/server/chunks/{920.js → 9920.js} +2 -2
- package/.next/server/functions-config-manifest.json +2 -1
- package/.next/server/instrumentation.js +1 -1
- package/.next/server/instrumentation.js.nft.json +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/middleware.js +185 -23
- package/.next/server/middleware.js.nft.json +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/preview-props.json +5 -0
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/server/webpack-runtime.js +1 -1
- package/.next/static/chunks/2716.44fdf1b21454b961.js +1 -0
- package/.next/static/chunks/316-698d10eb465bd8ef.js +1 -0
- package/.next/static/chunks/3794-0978865cda773008.js +52 -0
- package/.next/static/chunks/4673.e261707dce391be7.js +1 -0
- package/.next/static/chunks/4686-0cd87513f9ea3538.js +1 -0
- package/.next/static/chunks/4778.eddd3a5813f85824.js +1 -0
- package/.next/static/chunks/4bd1b696-e199e788e58acf79.js +1 -0
- package/.next/static/chunks/{5173.91ddb803785009f8.js → 5173.dfd1cbb795b9a21c.js} +1 -1
- package/.next/static/chunks/618.949981da99be7182.js +4 -0
- package/.next/static/chunks/7411.5f79671208209b13.js +1 -0
- package/.next/static/chunks/app/_global-error/page-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/events/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/side-chat/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/tree/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/agent/new/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/config/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/files/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/fs/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/git/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/github/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/growth/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/health/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/models/defaults/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/models/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/pi-settings/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/plugins/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/prompts/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/remote/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/security/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/export/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/sessions/import/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/sessions/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/skills/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/update/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/version/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/web-auth/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/api/workspaces/route-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/app/page-dc3cb41d91c7432e.js +1 -0
- package/.next/static/chunks/framework-47b723d27f4a4963.js +1 -0
- package/.next/static/chunks/main-5403f0764579e5fe.js +5 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-cbaaee0d7aacb85a.js +1 -0
- package/.next/static/chunks/webpack-2ac3d76b81895f5b.js +1 -0
- package/.next/static/css/9fc559c2c4ad1d0f.css +1 -0
- package/.next/static/css/c6e2227c36ec2fd6.css +1 -0
- package/.next/static/css/{2959bd6b626b7ffe.css → f15a853af8b5127f.css} +1 -1
- package/.next/static/hYPL1-dmhKFjLS3f5yoqN/_buildManifest.js +1 -0
- package/.next/types/app/api/agent/[id]/events/route.ts +3 -1
- package/.next/types/app/api/agent/[id]/route.ts +3 -1
- package/.next/types/app/api/agent/[id]/side-chat/route.ts +355 -0
- package/.next/types/app/api/agent/[id]/tree/route.ts +3 -1
- package/.next/types/app/api/agent/new/route.ts +3 -1
- package/.next/types/app/api/config/route.ts +3 -1
- package/.next/types/app/api/files/route.ts +3 -1
- package/.next/types/app/api/fs/route.ts +3 -1
- package/.next/types/app/api/git/route.ts +3 -1
- package/.next/types/app/api/github/route.ts +355 -0
- package/.next/types/app/api/growth/route.ts +3 -1
- package/.next/types/app/api/health/route.ts +3 -1
- package/.next/types/app/api/models/defaults/route.ts +355 -0
- package/.next/types/app/api/models/route.ts +3 -1
- package/.next/types/app/api/pi-settings/route.ts +3 -1
- package/.next/types/app/api/plugins/route.ts +3 -1
- package/.next/types/app/api/prompts/route.ts +3 -1
- package/.next/types/app/api/remote/route.ts +3 -1
- package/.next/types/app/api/security/route.ts +3 -1
- package/.next/types/app/api/sessions/[id]/export/route.ts +3 -1
- package/.next/types/app/api/sessions/[id]/route.ts +3 -1
- package/.next/types/app/api/sessions/import/route.ts +3 -1
- package/.next/types/app/api/sessions/route.ts +3 -1
- package/.next/types/app/api/skills/route.ts +3 -1
- package/.next/types/app/api/update/route.ts +3 -1
- package/.next/types/app/api/version/route.ts +3 -1
- package/.next/types/app/api/web-auth/route.ts +3 -1
- package/.next/types/app/api/workspaces/route.ts +3 -1
- package/.next/types/app/layout.ts +4 -1
- package/.next/types/app/login/page.ts +4 -1
- package/.next/types/app/page.ts +4 -1
- package/.next/types/cache-life.d.ts +11 -43
- package/.next/types/routes.d.ts +4 -1
- package/.next/types/validator.ts +86 -2
- package/next.config.ts +2 -0
- package/package.json +8 -2
- package/scripts/release.mjs +3 -7
- package/scripts/runtime/check-installed.mjs +66 -0
- package/scripts/runtime/pi-packages.mjs +51 -0
- package/scripts/verify-package.mjs +29 -7
- package/src/app/api/agent/[id]/side-chat/route.ts +77 -0
- package/src/app/api/files/route.ts +5 -2
- package/src/app/api/github/route.ts +26 -0
- package/src/app/api/models/defaults/route.ts +18 -0
- package/src/app/api/models/route.ts +24 -1
- package/src/app/globals.css +1748 -0
- package/src/components/AppShell.tsx +145 -43
- package/src/components/ChatInput.tsx +264 -79
- package/src/components/ChatWindow.tsx +13 -23
- package/src/components/ContextMeter.tsx +213 -144
- package/src/components/DrawerPanel.tsx +1 -1
- package/src/components/FileViewer.tsx +89 -8
- package/src/components/GitPanel.tsx +110 -40
- package/src/components/GrowthTree.tsx +30 -5
- package/src/components/ModelSelector.tsx +21 -5
- package/src/components/ProjectPanel.tsx +55 -30
- package/src/components/PromptPanel.tsx +2 -4
- package/src/components/ProviderSetupModal.module.css +20 -0
- package/src/components/ProviderSetupModal.tsx +134 -29
- package/src/components/SettingsPanel.tsx +252 -27
- package/src/components/TrajectoryView.tsx +4 -6
- package/src/components/chat/reading-anchor.ts +81 -0
- package/src/components/composer/ComposerImages.tsx +326 -0
- package/src/components/side-chat/SideChatPanel.module.css +131 -0
- package/src/components/side-chat/SideChatPanel.tsx +189 -0
- package/src/components/side-chat/icons.tsx +8 -0
- package/src/components/side-chat/request-history.ts +29 -0
- package/src/components/side-chat/store.ts +248 -0
- package/src/hooks/useFileViewer.ts +6 -4
- package/src/hooks/useGrowth.ts +3 -0
- package/src/hooks/usePiWeb.ts +12 -2
- package/src/hooks/useWorkspaceFiles.ts +93 -0
- package/src/i18n.tsx +150 -2
- package/src/lib/agent-manager.ts +135 -104
- package/src/lib/files-service.ts +166 -3
- package/src/lib/format.ts +56 -0
- package/src/lib/git-service.ts +45 -13
- package/src/lib/github-identity.ts +245 -0
- package/src/lib/growth-service.ts +6 -2
- package/src/lib/hero-model-memory.ts +28 -0
- package/src/lib/model-defaults.ts +105 -0
- package/src/lib/model-draft.ts +257 -9
- package/src/lib/model-endpoint.ts +23 -0
- package/src/lib/model-files.ts +33 -0
- package/src/lib/models-service.ts +240 -78
- package/src/lib/path-security.ts +24 -32
- package/src/lib/pi.ts +27 -2
- package/src/lib/process-format.ts +3 -2
- package/src/lib/session/metrics.ts +31 -0
- package/src/lib/session-reader.ts +1 -1
- package/src/lib/side-chat/context.ts +123 -0
- package/src/lib/side-chat/service.ts +191 -0
- package/src/lib/side-chat/types.ts +61 -0
- package/src/lib/thinking-level.ts +39 -0
- package/src/lib/trajectory.ts +17 -3
- package/src/lib/types.ts +12 -1
- package/src/lib/update-service.ts +4 -6
- package/src/lib/workspace-index.ts +270 -0
- package/.next/server/chunks/229.js +0 -1
- package/.next/server/chunks/393.js +0 -1
- package/.next/server/chunks/421.js +0 -1
- package/.next/server/chunks/430.js +0 -193
- package/.next/server/chunks/496.js +0 -1
- package/.next/server/chunks/662.js +0 -2
- package/.next/server/chunks/686.js +0 -1
- package/.next/server/chunks/741.js +0 -165
- package/.next/server/chunks/802.js +0 -2
- package/.next/server/chunks/807.js +0 -1
- package/.next/server/chunks/813.js +0 -1
- package/.next/server/chunks/924.js +0 -6
- package/.next/static/chunks/2716.7ad073038e94a802.js +0 -1
- package/.next/static/chunks/316-5ea443ee6b512aa9.js +0 -1
- package/.next/static/chunks/3794-3679f1c05160977c.js +0 -52
- package/.next/static/chunks/4673.8a5a9e6855350e34.js +0 -1
- package/.next/static/chunks/4686-882531aef1d2e250.js +0 -1
- package/.next/static/chunks/4778.305dc6abb9889627.js +0 -1
- package/.next/static/chunks/4bd1b696-93bf506b236d6248.js +0 -1
- package/.next/static/chunks/618.ee5d5bf1c647e483.js +0 -4
- package/.next/static/chunks/839.bfd2db36483faa3e.js +0 -1
- package/.next/static/chunks/app/_global-error/page-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/events/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/tree/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/agent/new/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/config/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/files/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/fs/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/git/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/growth/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/health/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/models/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/pi-settings/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/plugins/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/prompts/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/remote/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/security/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/export/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/sessions/import/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/skills/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/update/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/version/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/web-auth/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/workspaces/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/page-c04af72e549695bd.js +0 -1
- package/.next/static/chunks/framework-c4e651fe89edfa91.js +0 -1
- package/.next/static/chunks/main-6159b22a8cb0b7c5.js +0 -5
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/webpack-9b4bb574e5817654.js +0 -1
- package/.next/static/css/ea27b9e30f76303f.css +0 -1
- package/.next/static/sNX5GQkOWuK5YEID1hT-C/_buildManifest.js +0 -1
- /package/.next/static/{sNX5GQkOWuK5YEID1hT-C → hYPL1-dmhKFjLS3f5yoqN}/_ssgManifest.js +0 -0
package/src/lib/files-service.ts
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
* files-service:会话工作区的文件浏览(详情栏「文件」面板)。
|
|
3
3
|
* 安全边界:目标路径必须落在工作区根内(realpath 双重校验,防符号链接逃逸)。
|
|
4
4
|
*/
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
5
|
+
import { execFile, spawn } from "node:child_process";
|
|
6
6
|
import fs from "node:fs/promises";
|
|
7
7
|
import path from "node:path";
|
|
8
|
+
import { promisify } from "node:util";
|
|
8
9
|
import { BoundaryError, isPathInside, resolveWorkspacePath } from "./path-security";
|
|
9
10
|
import { getAgentDir } from "./pi";
|
|
11
|
+
import { resolveGitBin } from "./growth-service";
|
|
10
12
|
import { utf8Prefix } from "./text-sanitize";
|
|
11
13
|
|
|
14
|
+
const exec = promisify(execFile);
|
|
15
|
+
|
|
12
16
|
export interface FileEntry {
|
|
13
17
|
name: string;
|
|
14
18
|
kind: "dir" | "file";
|
|
@@ -29,11 +33,166 @@ const MAX_READ_BYTES = 256 * 1024;
|
|
|
29
33
|
/** 单次目录响应上限;后续条目通过 offset 分页获取。 */
|
|
30
34
|
const MAX_ENTRIES = 2000;
|
|
31
35
|
|
|
36
|
+
/**
|
|
37
|
+
* @ 引用的候选索引上限。一次拉给浏览器做本地模糊匹配,所以是一份完整清单:
|
|
38
|
+
* 每敲一个字符不再发请求(实测本仓库 700 余条 ≈ 30 KB)。
|
|
39
|
+
* 超大仓库截断到上限并在界面标出,不静默少候选。
|
|
40
|
+
*/
|
|
41
|
+
const INDEX_LIMIT = 20_000;
|
|
42
|
+
/** 非 git 工作区的退路:只跳过这些明显不该当候选的重目录,其余照常列举 */
|
|
43
|
+
const INDEX_SKIP_DIRS = new Set([".git", "node_modules", ".next", ".next-dev", ".venv", "venv", "__pycache__", "dist", "build", "out", "target", ".turbo", ".cache"]);
|
|
44
|
+
const INDEX_MAX_DEPTH = 12;
|
|
45
|
+
|
|
46
|
+
export interface WorkspaceIndex {
|
|
47
|
+
/** 工作区根下的相对路径("/" 分隔)。只给文件:文件夹由前端从文件路径推导 */
|
|
48
|
+
files: string[];
|
|
49
|
+
/** git = `git ls-files`(尊重 .gitignore);disk = 不是仓库时的目录遍历退路 */
|
|
50
|
+
source: "git" | "disk";
|
|
51
|
+
/** 候选被上限截断:匹配结果可能不全,界面要说明 */
|
|
52
|
+
truncated: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** `git ls-files -z` 输出 → 相对路径数组(去重) */
|
|
56
|
+
export function parseLsFiles(raw: string): string[] {
|
|
57
|
+
const seen = new Set<string>();
|
|
58
|
+
for (const entry of raw.split("\0")) {
|
|
59
|
+
if (!entry) continue;
|
|
60
|
+
const rel = entry.replace(/\\/g, "/");
|
|
61
|
+
if (rel.startsWith("/") || rel.split("/").includes("..")) continue;
|
|
62
|
+
seen.add(rel);
|
|
63
|
+
}
|
|
64
|
+
return [...seen];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 不是仓库(或机器上没有 git)时的退路:广度优先遍历,只收文件 */
|
|
68
|
+
async function walkWorkspaceFiles(root: string): Promise<{ files: string[]; truncated: boolean }> {
|
|
69
|
+
const files: string[] = [];
|
|
70
|
+
let queue: Array<{ rel: string; depth: number }> = [{ rel: "", depth: 0 }];
|
|
71
|
+
while (queue.length) {
|
|
72
|
+
const next: typeof queue = [];
|
|
73
|
+
for (const dir of queue) {
|
|
74
|
+
const entries = await fs.readdir(path.join(root, dir.rel), { withFileTypes: true }).catch(() => []);
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const rel = dir.rel ? `${dir.rel}/${entry.name}` : entry.name;
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
if (dir.depth + 1 <= INDEX_MAX_DEPTH && !INDEX_SKIP_DIRS.has(entry.name)) next.push({ rel, depth: dir.depth + 1 });
|
|
79
|
+
} else if (entry.isFile()) {
|
|
80
|
+
files.push(rel);
|
|
81
|
+
if (files.length > INDEX_LIMIT) return { files: files.slice(0, INDEX_LIMIT), truncated: true };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
queue = next;
|
|
86
|
+
}
|
|
87
|
+
return { files, truncated: false };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* git 失败时只有「不是仓库」「没装 git」两种该退回遍历——界面会把 disk 来源解释成这两种情况。
|
|
92
|
+
* 其它失败(safe.directory 拒绝、索引损坏、超时)照实报错:退回遍历会把 .gitignore 掉的文件混进候选,还给出错误的解释。
|
|
93
|
+
*/
|
|
94
|
+
function gitIndexFailure(error: unknown): "fallback" | Error {
|
|
95
|
+
const e = error as NodeJS.ErrnoException & { stderr?: string; killed?: boolean };
|
|
96
|
+
if (e.code === "ENOENT") return "fallback";
|
|
97
|
+
const stderr = String(e.stderr ?? "");
|
|
98
|
+
if (/not a git repository/i.test(stderr)) return "fallback";
|
|
99
|
+
if (e.killed) return new Error("git ls-files timed out");
|
|
100
|
+
const fatal = stderr.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
101
|
+
return new Error(`git ls-files failed: ${fatal.find((line) => line.startsWith("fatal:")) ?? fatal[0] ?? e.message}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @ 引用的候选清单:git 可见的文件(含未被忽略的未跟踪文件)。
|
|
106
|
+
* `git ls-files` 的路径相对当前目录,所以 cwd 就是工作区根,输出天然是工作区相对路径。
|
|
107
|
+
*/
|
|
108
|
+
export async function listWorkspaceIndex(cwdValue: unknown): Promise<WorkspaceIndex> {
|
|
109
|
+
const root = await resolveWorkspacePath(cwdValue);
|
|
110
|
+
const git = await resolveGitBin();
|
|
111
|
+
try {
|
|
112
|
+
const { stdout } = await exec(git, ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
113
|
+
cwd: root,
|
|
114
|
+
timeout: 15_000,
|
|
115
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
116
|
+
windowsHide: true,
|
|
117
|
+
// LC_ALL=C:报错固定为英文,「不是仓库」才认得出来(中文界面的 git 会翻译这句);-z 输出的路径不受影响
|
|
118
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0", LC_ALL: "C" },
|
|
119
|
+
});
|
|
120
|
+
const files = parseLsFiles(stdout).sort((a, b) => a.localeCompare(b));
|
|
121
|
+
return { files: files.slice(0, INDEX_LIMIT), source: "git", truncated: files.length > INDEX_LIMIT };
|
|
122
|
+
} catch (error) {
|
|
123
|
+
const failure = gitIndexFailure(error);
|
|
124
|
+
if (failure !== "fallback") throw failure;
|
|
125
|
+
const walked = await walkWorkspaceFiles(root);
|
|
126
|
+
return { files: walked.files.sort((a, b) => a.localeCompare(b)), source: "disk", truncated: walked.truncated };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
32
131
|
function toRel(root: string, target: string): string {
|
|
33
132
|
const rel = path.relative(root, target).replace(/\\/g, "/");
|
|
34
133
|
return rel === "" ? "." : rel;
|
|
35
134
|
}
|
|
36
135
|
|
|
136
|
+
/** `git check-ignore -z` 输出 → 相对路径数组 */
|
|
137
|
+
export function parseCheckIgnore(raw: string): string[] {
|
|
138
|
+
return raw.split("\u0000").filter(Boolean).map((entry) => entry.split("\\").join("/"));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 问 git 这个目录里哪些条目被 .gitignore 忽略(且未被跟踪)。
|
|
143
|
+
* 用 check-ignore --stdin 一次问完整个目录:0 = 有被忽略的,1 = 一条都没有,128 = 不是仓库。
|
|
144
|
+
* 条目多时命令行会超长,所以走 stdin 不走参数(单页最多 2000 条)。
|
|
145
|
+
*/
|
|
146
|
+
function checkIgnore(root: string, git: string, rels: string[]): Promise<string> {
|
|
147
|
+
return new Promise((resolve, reject) => {
|
|
148
|
+
const child = spawn(git, ["check-ignore", "-z", "--stdin"], {
|
|
149
|
+
cwd: root,
|
|
150
|
+
windowsHide: true,
|
|
151
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0", LC_ALL: "C" },
|
|
152
|
+
});
|
|
153
|
+
let out = "";
|
|
154
|
+
let err = "";
|
|
155
|
+
let inputError: Error | undefined;
|
|
156
|
+
// Pipe errors are asynchronous: try/catch around end() cannot handle EPIPE.
|
|
157
|
+
child.stdin.on("error", (error) => { inputError = error; });
|
|
158
|
+
const timer = setTimeout(() => { child.kill(); reject(new Error("git check-ignore timed out")); }, 10_000);
|
|
159
|
+
child.stdout.on("data", (chunk: Buffer) => { out += chunk.toString("utf8"); });
|
|
160
|
+
child.stderr.on("data", (chunk: Buffer) => { err += chunk.toString("utf8"); });
|
|
161
|
+
child.on("error", (error) => { clearTimeout(timer); reject(error); });
|
|
162
|
+
child.on("close", (code) => {
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
if (inputError) reject(inputError);
|
|
165
|
+
else if (code === 0) resolve(out);
|
|
166
|
+
else if (code === 1) resolve("");
|
|
167
|
+
else reject(new Error(err.trim() || `git check-ignore exited with ${code}`));
|
|
168
|
+
});
|
|
169
|
+
try {
|
|
170
|
+
child.stdin.end(`${rels.join("\u0000")}\u0000`);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
inputError = error instanceof Error ? error : new Error(String(error));
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 这个目录里该从列表里去掉的名字:被 .gitignore 忽略的(node_modules/、.next、.claude 这类的构建产物与私有目录)。
|
|
179
|
+
* 判据用 git 自己的规则,和快照树、@ 引用清单同一口径;不是仓库 / 没装 git / git 出错一律不过滤
|
|
180
|
+
* (宁可多显示,也不能把用户的项目文件藏起来)。
|
|
181
|
+
*/
|
|
182
|
+
async function ignoredNames(root: string, dirRel: string, names: string[]): Promise<Set<string>> {
|
|
183
|
+
if (!names.length) return new Set();
|
|
184
|
+
try {
|
|
185
|
+
const git = await resolveGitBin();
|
|
186
|
+
const rels = names.map((name) => (dirRel ? `${dirRel}/${name}` : name));
|
|
187
|
+
const raw = await checkIgnore(root, git, rels);
|
|
188
|
+
// 输出是相对仓库根的路径:减掉当前目录前缀就回到名字集合
|
|
189
|
+
const prefix = dirRel ? `${dirRel}/` : "";
|
|
190
|
+
return new Set(parseCheckIgnore(raw).map((entry) => (prefix && entry.startsWith(prefix) ? entry.slice(prefix.length) : entry)));
|
|
191
|
+
} catch {
|
|
192
|
+
return new Set();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
37
196
|
export async function listWorkspaceDir(cwdValue: unknown, relPath: unknown, offsetValue: unknown = 0): Promise<DirListing> {
|
|
38
197
|
const root = await resolveWorkspacePath(cwdValue);
|
|
39
198
|
const offset = offsetValue === null || offsetValue === "" ? 0 : Number(offsetValue);
|
|
@@ -75,8 +234,12 @@ export async function listWorkspaceDir(cwdValue: unknown, relPath: unknown, offs
|
|
|
75
234
|
if (a.kind !== b.kind) return a.kind === "dir" ? -1 : 1;
|
|
76
235
|
return a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: "base" });
|
|
77
236
|
});
|
|
78
|
-
|
|
79
|
-
const
|
|
237
|
+
// .gitignore 忽略的条目(node_modules/、.next/ 这些)不进列表:和快照树、@ 引用清单同一口径
|
|
238
|
+
const relForIgnore = path.relative(root, real).replace(/\\/g, "/");
|
|
239
|
+
const ignored = await ignoredNames(root, relForIgnore, entries.map((entry) => entry.name));
|
|
240
|
+
const visible = ignored.size ? entries.filter((entry) => !ignored.has(entry.name)) : entries;
|
|
241
|
+
const nextOffset = offset + MAX_ENTRIES < visible.length ? offset + MAX_ENTRIES : null;
|
|
242
|
+
const page = visible.slice(offset, offset + MAX_ENTRIES);
|
|
80
243
|
for (let i = 0; i < page.length; i += 64) {
|
|
81
244
|
await Promise.all(page.slice(i, i + 64).map(async (entry) => {
|
|
82
245
|
if (entry.kind !== "file" || entry.size !== undefined) return;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 全站数字显示格式。
|
|
3
|
+
*
|
|
4
|
+
* 精度口径(用户要求「精确到小数点后两位」):
|
|
5
|
+
* - 比率(缓存命中 / 上下文占用)、速率(tok/s)、秒级时长:一律两位小数;
|
|
6
|
+
* - token 量:≥1000 用 K、≥100 万用 M,都两位小数;<1000 原样整数(计数没有小数含义);
|
|
7
|
+
* - 轮次 / 步数 / 文件数等计数:整数;
|
|
8
|
+
* - 分钟级时长保持 `19m30s`(`19m30.42s` 更难读)、金额保持四位(`$0.0123` 是惯例)。
|
|
9
|
+
*
|
|
10
|
+
* 以前 ChatWindow / ContextMeter / TrajectoryView 各有一份 fmtTok,同一个数字在三处
|
|
11
|
+
* 显示不同(262400 → `262K` / `262.4K` / `262.4K`;53000000 → `53M` / `53000.0K` / `53.0M`),
|
|
12
|
+
* 统一到这里。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** token 量:K / M 两位小数,<1000 整数 */
|
|
16
|
+
export function fmtTokens(value: number): string {
|
|
17
|
+
if (!Number.isFinite(value)) return "—";
|
|
18
|
+
if (Math.abs(value) < 1_000) return String(Math.round(value));
|
|
19
|
+
// 先按 K 缩放;四舍五入到两位小数后已到 1000.00K 就升级成 M,避免出现「1000.00K」
|
|
20
|
+
const k = value / 1_000;
|
|
21
|
+
if (Math.abs(Number(k.toFixed(2))) < 1_000) return `${k.toFixed(2)}K`;
|
|
22
|
+
return `${(value / 1_000_000).toFixed(2)}M`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 计数(轮次 / 步数 / 文件数):整数 */
|
|
26
|
+
export function fmtCount(value: number): string {
|
|
27
|
+
return Number.isFinite(value) ? String(Math.round(value)) : "—";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 百分比:入参是 0–100 的数值,输出两位小数 */
|
|
31
|
+
export function fmtPercent(percent: number): string {
|
|
32
|
+
return `${(Number.isFinite(percent) ? percent : 0).toFixed(2)}%`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 速率:tok/s,两位小数 */
|
|
36
|
+
export function fmtThroughput(tokensPerSecond: number): string {
|
|
37
|
+
return (Number.isFinite(tokensPerSecond) ? tokensPerSecond : 0).toFixed(2);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 时长:不足 60 秒两位小数(`5.32s`),到分钟级用 `19m30s`(秒级再补小数只会更难读)。
|
|
42
|
+
*/
|
|
43
|
+
export function fmtDuration(ms: number): string {
|
|
44
|
+
const seconds = Math.max(0, Number.isFinite(ms) ? ms : 0) / 1_000;
|
|
45
|
+
if (seconds < 60) return `${seconds.toFixed(2)}s`;
|
|
46
|
+
const whole = Math.round(seconds);
|
|
47
|
+
return `${Math.floor(whole / 60)}m${whole % 60}s`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 文件大小:B 整数,KB / MB 两位小数 */
|
|
51
|
+
export function fmtBytes(bytes: number): string {
|
|
52
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
|
|
53
|
+
if (bytes < 1_024) return `${Math.round(bytes)} B`;
|
|
54
|
+
if (bytes < 1_024 * 1_024) return `${(bytes / 1_024).toFixed(2)} KB`;
|
|
55
|
+
return `${(bytes / 1_024 / 1_024).toFixed(2)} MB`;
|
|
56
|
+
}
|
package/src/lib/git-service.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { execFile } from "node:child_process";
|
|
|
7
7
|
import fs from "node:fs/promises";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { promisify } from "node:util";
|
|
10
|
-
import { resolveGitBin } from "./growth-service";
|
|
10
|
+
import { parseNumstat, resolveGitBin } from "./growth-service";
|
|
11
11
|
import { BoundaryError, isPathInside, resolveWorkspacePath } from "./path-security";
|
|
12
12
|
|
|
13
13
|
const exec = promisify(execFile);
|
|
@@ -76,7 +76,8 @@ function kindOf(x: string, y: string): GitFileEntry["kind"] {
|
|
|
76
76
|
return "modified";
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
/** 解析仓库根(GitHub 身份解析也要用,所以导出) */
|
|
80
|
+
export async function repoRoot(cwd: string): Promise<{ available: boolean; root: string | null }> {
|
|
80
81
|
try {
|
|
81
82
|
const out = (await runGit(cwd, ["rev-parse", "--show-toplevel"])).trim();
|
|
82
83
|
return { available: true, root: out ? path.resolve(out) : null };
|
|
@@ -187,11 +188,34 @@ export interface GitDiffResult {
|
|
|
187
188
|
patch: string;
|
|
188
189
|
truncated: boolean;
|
|
189
190
|
binary: boolean;
|
|
191
|
+
/** 完整 diff 的 Git numstat,独立于用于展示的 patch 截断。二进制不计行数。 */
|
|
192
|
+
stats: { add: number; del: number };
|
|
190
193
|
}
|
|
191
194
|
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
|
|
195
|
+
async function fullDiffStats(root: string, args: string[], noIndex = false): Promise<{ stats: GitDiffResult["stats"]; binary: boolean }> {
|
|
196
|
+
let raw: string;
|
|
197
|
+
try {
|
|
198
|
+
raw = await runGit(root, args);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
// --no-index 的退出码 1 表示有差异,stdout 仍是有效的 numstat。
|
|
201
|
+
const result = error as { code?: number; stdout?: string };
|
|
202
|
+
if (!noIndex || result.code !== 1 || typeof result.stdout !== "string" || !result.stdout) throw error;
|
|
203
|
+
raw = result.stdout;
|
|
204
|
+
}
|
|
205
|
+
let add = 0;
|
|
206
|
+
let del = 0;
|
|
207
|
+
let binary = false;
|
|
208
|
+
for (const entry of parseNumstat(raw).values()) {
|
|
209
|
+
add += entry.add;
|
|
210
|
+
del += entry.del;
|
|
211
|
+
binary ||= entry.binary;
|
|
212
|
+
}
|
|
213
|
+
return { stats: { add, del }, binary };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function truncate(patch: string, stats: GitDiffResult["stats"]): GitDiffResult {
|
|
217
|
+
if (Buffer.byteLength(patch) <= MAX_DIFF_BYTES) return { patch, truncated: false, binary: false, stats };
|
|
218
|
+
return { patch: Buffer.from(patch).subarray(0, MAX_DIFF_BYTES).toString("utf8"), truncated: true, binary: false, stats };
|
|
195
219
|
}
|
|
196
220
|
|
|
197
221
|
function assertRepoRelative(root: string, relPath: unknown): string {
|
|
@@ -215,6 +239,8 @@ export async function gitDiff(cwdValue: unknown, relPath: unknown, mode: unknown
|
|
|
215
239
|
if (!isPathInside(realRoot, realFile)) throw new BoundaryError("path escapes the repository");
|
|
216
240
|
const stat = await fs.stat(realFile).catch(() => null);
|
|
217
241
|
if (!stat?.isFile()) throw new BoundaryError("file not found");
|
|
242
|
+
const { stats, binary } = await fullDiffStats(root, ["diff", "--no-index", "--no-ext-diff", "--no-textconv", "--numstat", "-z", "--", "/dev/null", realFile], true);
|
|
243
|
+
if (binary) return { patch: "", truncated: false, binary: true, stats };
|
|
218
244
|
const buffer = Buffer.alloc(Math.min(stat.size, MAX_DIFF_BYTES + 1));
|
|
219
245
|
const handle = await fs.open(realFile, "r");
|
|
220
246
|
let bytesRead = 0;
|
|
@@ -228,8 +254,7 @@ export async function gitDiff(cwdValue: unknown, relPath: unknown, mode: unknown
|
|
|
228
254
|
await handle.close();
|
|
229
255
|
}
|
|
230
256
|
const head = buffer.subarray(0, bytesRead);
|
|
231
|
-
|
|
232
|
-
const text = head.subarray(0, MAX_DIFF_BYTES).toString("utf8").replace(/\r/g, "");
|
|
257
|
+
const text = head.subarray(0, MAX_DIFF_BYTES).toString("utf8").replace(/\r\n/g, "\n");
|
|
233
258
|
const lines = text.split("\n");
|
|
234
259
|
if (lines[lines.length - 1] === "") lines.pop();
|
|
235
260
|
const body = lines.map((l) => `+${l}`).join("\n");
|
|
@@ -237,12 +262,16 @@ export async function gitDiff(cwdValue: unknown, relPath: unknown, mode: unknown
|
|
|
237
262
|
patch: `--- /dev/null\n+++ b/${rel}\n@@ -0,0 +1,${lines.length} @@\n${body}\n`,
|
|
238
263
|
truncated: stat.size > MAX_DIFF_BYTES,
|
|
239
264
|
binary: false,
|
|
265
|
+
stats,
|
|
240
266
|
};
|
|
241
267
|
}
|
|
242
|
-
const
|
|
243
|
-
const out = await
|
|
244
|
-
|
|
245
|
-
|
|
268
|
+
const scope = mode === "staged" ? ["--cached"] : [];
|
|
269
|
+
const [out, counts] = await Promise.all([
|
|
270
|
+
runGit(root, ["diff", ...scope, "--no-color", "--no-ext-diff", "--no-textconv", "--", rel], 8 * 1024 * 1024),
|
|
271
|
+
fullDiffStats(root, ["diff", ...scope, "--no-ext-diff", "--no-textconv", "--numstat", "-z", "--", rel]),
|
|
272
|
+
]);
|
|
273
|
+
if (counts.binary) return { patch: "", truncated: false, binary: true, stats: counts.stats };
|
|
274
|
+
return truncate(out, counts.stats);
|
|
246
275
|
}
|
|
247
276
|
|
|
248
277
|
/** 单次提交的内容(含各文件 patch) */
|
|
@@ -252,6 +281,9 @@ export async function gitShow(cwdValue: unknown, hash: unknown): Promise<GitDiff
|
|
|
252
281
|
if (!available || !root) throw new BoundaryError("not a git repository");
|
|
253
282
|
if (typeof hash !== "string" || !/^[0-9a-f]{4,40}$/i.test(hash)) throw new BoundaryError("invalid commit hash");
|
|
254
283
|
const title = (await runGit(root, ["log", "-1", "--pretty=%h %s%n%an · %cr", hash])).trim();
|
|
255
|
-
const out = await
|
|
256
|
-
|
|
284
|
+
const [out, counts] = await Promise.all([
|
|
285
|
+
runGit(root, ["show", "--no-color", "--no-ext-diff", "--no-textconv", "--format=", "--patch", hash], 8 * 1024 * 1024),
|
|
286
|
+
fullDiffStats(root, ["show", "--no-ext-diff", "--no-textconv", "--format=", "--numstat", "-z", hash]),
|
|
287
|
+
]);
|
|
288
|
+
return { title, ...truncate(out, counts.stats), binary: counts.binary && !out.includes("@@ ") };
|
|
257
289
|
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub 身份:Git 面板头部显示「头像 + 账号」。
|
|
3
|
+
*
|
|
4
|
+
* 只读,不碰任何写操作,也不把 token 交给浏览器。三档降级:
|
|
5
|
+
*
|
|
6
|
+
* 1. `gh api user` —— 本机 gh 已登录时拿到的是**当前登录账号**(最准);
|
|
7
|
+
* 2. 仓库 `origin` remote 的 owner —— 没装 gh / 没登录也能用,头像是免登录的
|
|
8
|
+
* `github.com/<owner>.png`;但拿到的可能是「仓库归属者」而不是登录账号;
|
|
9
|
+
* 3. `git config user.name` —— 纯本地身份,没有头像,只作文字兜底。
|
|
10
|
+
*
|
|
11
|
+
* 两条网络路径实测:`gh api user` 约 1.1s、头像约 2.2s,所以结果缓存 12 小时
|
|
12
|
+
* (内存 + 落盘,PiWeb 重启后仍然即时)。头像取到后转成 data URL 随身份一起返回,
|
|
13
|
+
* 前端不用再发第二次请求,断网也能显示。
|
|
14
|
+
*
|
|
15
|
+
* 任何一步失败都只降级,不抛错:Git 面板不该因为 GitHub 不通而少东西。
|
|
16
|
+
*/
|
|
17
|
+
import { execFile } from "node:child_process";
|
|
18
|
+
import fs from "node:fs/promises";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { promisify } from "node:util";
|
|
21
|
+
import { resolveGitBin } from "./growth-service";
|
|
22
|
+
import { getAgentDir } from "./pi";
|
|
23
|
+
|
|
24
|
+
const exec = promisify(execFile);
|
|
25
|
+
|
|
26
|
+
export interface GithubIdentity {
|
|
27
|
+
login: string;
|
|
28
|
+
/** gh 里的显示名(如「心安」),没有就省略 */
|
|
29
|
+
name?: string;
|
|
30
|
+
/** data URL;取不到头像时省略,UI 退回首字母 */
|
|
31
|
+
avatar?: string;
|
|
32
|
+
/** 账号主页,点击可跳转 */
|
|
33
|
+
url?: string;
|
|
34
|
+
/** `owner/repo`(来自 origin remote),没配 remote 就没有 */
|
|
35
|
+
repo?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const TTL_MS = 12 * 60 * 60 * 1_000;
|
|
39
|
+
const GH_TIMEOUT_MS = 8_000;
|
|
40
|
+
const AVATAR_TIMEOUT_MS = 8_000;
|
|
41
|
+
const AVATAR_SIZE = 64;
|
|
42
|
+
const AVATAR_MAX_BYTES = 256 * 1_024;
|
|
43
|
+
const CACHE_FILENAME = "piweb-github-identity.json";
|
|
44
|
+
// 身份字段一变就提版本:旧的缓存条目直接作废重取,避免返回已经删掉的字段
|
|
45
|
+
const CACHE_VERSION = 2;
|
|
46
|
+
|
|
47
|
+
interface CacheFile {
|
|
48
|
+
version: number;
|
|
49
|
+
entries: Record<string, { at: number; value: GithubIdentity | null }>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const memory = new Map<string, { at: number; value: GithubIdentity | null }>();
|
|
53
|
+
const inflight = new Map<string, Promise<GithubIdentity | null>>();
|
|
54
|
+
let diskLoaded = false;
|
|
55
|
+
let diskWrite: Promise<void> | null = null;
|
|
56
|
+
|
|
57
|
+
function cachePath(): string {
|
|
58
|
+
return path.join(getAgentDir(), CACHE_FILENAME);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 离线模式(SDK 也认这个变量):不发任何网络请求,只走本地能算出来的那一档 */
|
|
62
|
+
function offline(): boolean {
|
|
63
|
+
const value = process.env.PI_OFFLINE;
|
|
64
|
+
if (!value || value === "0") return false;
|
|
65
|
+
return value.toLowerCase() !== "false";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function loadDisk(): Promise<void> {
|
|
69
|
+
if (diskLoaded) return;
|
|
70
|
+
diskLoaded = true;
|
|
71
|
+
try {
|
|
72
|
+
const raw = JSON.parse(await fs.readFile(cachePath(), "utf8")) as CacheFile;
|
|
73
|
+
if (raw?.version !== CACHE_VERSION || !raw.entries || typeof raw.entries !== "object") return;
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
for (const [key, entry] of Object.entries(raw.entries)) {
|
|
76
|
+
if (!entry || typeof entry.at !== "number" || now - entry.at >= TTL_MS) continue;
|
|
77
|
+
if (!memory.has(key)) memory.set(key, entry);
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
/* 没有缓存 / 读不了都当第一次 */
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function persist(): void {
|
|
85
|
+
if (diskWrite) return;
|
|
86
|
+
diskWrite = (async () => {
|
|
87
|
+
try {
|
|
88
|
+
const entries: CacheFile["entries"] = {};
|
|
89
|
+
for (const [key, entry] of memory) entries[key] = entry;
|
|
90
|
+
await fs.writeFile(cachePath(), JSON.stringify({ version: CACHE_VERSION, entries }), "utf8");
|
|
91
|
+
} catch {
|
|
92
|
+
/* 写不了就只在内存里缓存 */
|
|
93
|
+
} finally {
|
|
94
|
+
diskWrite = null;
|
|
95
|
+
}
|
|
96
|
+
})();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 解析 gh 可执行文件(PATH 找不到时退回常见安装位置);结果缓存 */
|
|
100
|
+
let ghBin: string | null | undefined;
|
|
101
|
+
async function resolveGhBin(): Promise<string | null> {
|
|
102
|
+
if (ghBin !== undefined) return ghBin;
|
|
103
|
+
const candidates = process.platform === "win32"
|
|
104
|
+
? ["gh", path.join(process.env.ProgramFiles ?? "C:\\Program Files", "GitHub CLI", "gh.exe"), path.join(process.env.LOCALAPPDATA ?? "", "Programs", "GitHub CLI", "gh.exe")]
|
|
105
|
+
: ["gh", "/opt/homebrew/bin/gh", "/usr/local/bin/gh", "/usr/bin/gh"];
|
|
106
|
+
for (const candidate of candidates) {
|
|
107
|
+
if (!candidate) continue;
|
|
108
|
+
const ok = await exec(candidate, ["--version"], { timeout: 5_000, windowsHide: true }).then(() => true, () => false);
|
|
109
|
+
if (ok) {
|
|
110
|
+
ghBin = candidate;
|
|
111
|
+
return ghBin;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
ghBin = null;
|
|
115
|
+
return ghBin;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function git(cwd: string, args: string[]): Promise<string> {
|
|
119
|
+
const out = await exec(await resolveGitBin(), args, {
|
|
120
|
+
cwd,
|
|
121
|
+
timeout: 15_000,
|
|
122
|
+
maxBuffer: 1024 * 1_024,
|
|
123
|
+
windowsHide: true,
|
|
124
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0" },
|
|
125
|
+
});
|
|
126
|
+
return out.stdout;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 从 remote URL 里取 owner 与仓库名:支持 https / ssh / git:// 三种写法 */
|
|
130
|
+
export function parseGithubRemote(remote: string | undefined | null): { owner: string; repo?: string } | null {
|
|
131
|
+
if (!remote) return null;
|
|
132
|
+
const trimmed = remote.trim().replace(/\.git$/, "");
|
|
133
|
+
const m = trimmed.match(/github\.com[:/]+([^/]+)(?:\/([^/]+))?/i);
|
|
134
|
+
if (!m?.[1]) return null;
|
|
135
|
+
return { owner: m[1], repo: m[2] ? `${m[1]}/${m[2]}` : undefined };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function ghAccount(cwd: string): Promise<{ login: string; name?: string; url?: string } | null> {
|
|
139
|
+
if (offline()) return null;
|
|
140
|
+
const bin = await resolveGhBin();
|
|
141
|
+
if (!bin) return null;
|
|
142
|
+
try {
|
|
143
|
+
const { stdout } = await exec(bin, ["api", "user"], {
|
|
144
|
+
cwd,
|
|
145
|
+
timeout: GH_TIMEOUT_MS,
|
|
146
|
+
maxBuffer: 1024 * 1_024,
|
|
147
|
+
windowsHide: true,
|
|
148
|
+
env: { ...process.env, GH_PROMPT_DISABLED: "1", GH_NO_UPDATE_NOTIFIER: "1" },
|
|
149
|
+
});
|
|
150
|
+
const parsed = JSON.parse(stdout) as { login?: unknown; name?: unknown; html_url?: unknown };
|
|
151
|
+
if (typeof parsed.login !== "string" || !parsed.login) return null;
|
|
152
|
+
return {
|
|
153
|
+
login: parsed.login,
|
|
154
|
+
name: typeof parsed.name === "string" && parsed.name.trim() ? parsed.name.trim() : undefined,
|
|
155
|
+
url: typeof parsed.html_url === "string" ? parsed.html_url : undefined,
|
|
156
|
+
};
|
|
157
|
+
} catch {
|
|
158
|
+
// 没网 / 没登录 / token 失效:交给下一档
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function avatarDataUrl(owner: string): Promise<string | undefined> {
|
|
164
|
+
if (offline()) return undefined;
|
|
165
|
+
try {
|
|
166
|
+
const res = await fetch(`https://github.com/${encodeURIComponent(owner)}.png?size=${AVATAR_SIZE}`, {
|
|
167
|
+
signal: AbortSignal.timeout(AVATAR_TIMEOUT_MS),
|
|
168
|
+
headers: { Accept: "image/*" },
|
|
169
|
+
});
|
|
170
|
+
if (!res.ok) return undefined;
|
|
171
|
+
const type = res.headers.get("content-type") ?? "image/png";
|
|
172
|
+
if (!type.startsWith("image/")) return undefined;
|
|
173
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
174
|
+
if (!buffer.length || buffer.length > AVATAR_MAX_BYTES) return undefined;
|
|
175
|
+
return `data:${type};base64,${buffer.toString("base64")}`;
|
|
176
|
+
} catch {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function compute(root: string): Promise<GithubIdentity | null> {
|
|
182
|
+
let remote: { owner: string; repo?: string } | null = null;
|
|
183
|
+
try {
|
|
184
|
+
remote = parseGithubRemote(await git(root, ["remote", "get-url", "origin"]));
|
|
185
|
+
} catch {
|
|
186
|
+
/* 没有 origin 就往下走 */
|
|
187
|
+
}
|
|
188
|
+
const account = await ghAccount(root);
|
|
189
|
+
if (account) {
|
|
190
|
+
return {
|
|
191
|
+
login: account.login,
|
|
192
|
+
name: account.name,
|
|
193
|
+
url: account.url ?? `https://github.com/${account.login}`,
|
|
194
|
+
avatar: await avatarDataUrl(account.login),
|
|
195
|
+
repo: remote?.repo,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (remote) {
|
|
199
|
+
return { login: remote.owner, url: `https://github.com/${remote.owner}`, avatar: await avatarDataUrl(remote.owner), repo: remote.repo };
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
const name = (await git(root, ["config", "user.name"])).trim();
|
|
203
|
+
if (name) return { login: name };
|
|
204
|
+
} catch {
|
|
205
|
+
/* 什么都没配 */
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function start(root: string): Promise<GithubIdentity | null> {
|
|
211
|
+
const existing = inflight.get(root);
|
|
212
|
+
if (existing) return existing;
|
|
213
|
+
const task = compute(root)
|
|
214
|
+
.then((value) => {
|
|
215
|
+
memory.set(root, { at: Date.now(), value });
|
|
216
|
+
persist();
|
|
217
|
+
return value;
|
|
218
|
+
})
|
|
219
|
+
.catch(() => null)
|
|
220
|
+
.finally(() => inflight.delete(root)) as Promise<GithubIdentity | null>;
|
|
221
|
+
inflight.set(root, task);
|
|
222
|
+
return task;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 取仓库的 GitHub 身份。命中缓存立刻返回;否则**后台**刷新并返回已过期的旧值(或 null),
|
|
227
|
+
* 不会让调用方等网络。要拿首次结果用 `awaitGithubIdentity`。
|
|
228
|
+
*/
|
|
229
|
+
export function githubIdentity(root: string | null): GithubIdentity | null {
|
|
230
|
+
if (!root) return null;
|
|
231
|
+
void loadDisk();
|
|
232
|
+
const hit = memory.get(root);
|
|
233
|
+
if (hit && Date.now() - hit.at < TTL_MS) return hit.value;
|
|
234
|
+
void start(root);
|
|
235
|
+
return hit?.value ?? null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** 等首次解析完成(面板刚打开、缓存还冷时用;调用方自己控制超时) */
|
|
239
|
+
export async function awaitGithubIdentity(root: string | null): Promise<GithubIdentity | null> {
|
|
240
|
+
if (!root) return null;
|
|
241
|
+
await loadDisk();
|
|
242
|
+
const hit = memory.get(root);
|
|
243
|
+
if (hit && Date.now() - hit.at < TTL_MS) return hit.value;
|
|
244
|
+
return start(root);
|
|
245
|
+
}
|
|
@@ -433,8 +433,12 @@ export function parseNumstat(raw: string): Map<string, { add: number; del: numbe
|
|
|
433
433
|
for (let i = 0; i < tokens.length; i += 1) {
|
|
434
434
|
const t = tokens[i];
|
|
435
435
|
if (!t) continue;
|
|
436
|
-
const
|
|
437
|
-
|
|
436
|
+
const first = t.indexOf("\t");
|
|
437
|
+
const second = t.indexOf("\t", first + 1);
|
|
438
|
+
if (first < 0 || second < 0) continue;
|
|
439
|
+
const a = t.slice(0, first);
|
|
440
|
+
const d = t.slice(first + 1, second);
|
|
441
|
+
const rest = t.slice(second + 1);
|
|
438
442
|
const binary = a === "-" || d === "-";
|
|
439
443
|
const entry = { add: binary ? 0 : Number(a) || 0, del: binary ? 0 : Number(d) || 0, binary };
|
|
440
444
|
if (rest === undefined || rest === "") {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 新会话页记住的模型:按工作区存在这个浏览器的 localStorage 里(key 为 piweb.heroModel.<工作区>)。
|
|
3
|
+
* 只认工作区自己记住的那一条;没有就是没选过,不回落到别的工作区,也不替用户预选。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface HeroModelChoice {
|
|
7
|
+
provider: string;
|
|
8
|
+
id: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const KEY_PREFIX = "piweb.heroModel.";
|
|
12
|
+
|
|
13
|
+
/** 读某个工作区记住的模型;没有、或内容损坏,都当作没选过 */
|
|
14
|
+
export function readRememberedHeroModel(storage: Pick<Storage, "getItem">, cwd: string): HeroModelChoice | null {
|
|
15
|
+
const raw = storage.getItem(`${KEY_PREFIX}${cwd}`);
|
|
16
|
+
if (!raw) return null;
|
|
17
|
+
try {
|
|
18
|
+
const value = JSON.parse(raw) as Partial<HeroModelChoice> | null;
|
|
19
|
+
return value && typeof value.provider === "string" && typeof value.id === "string" ? { provider: value.provider, id: value.id } : null;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 记住某个工作区选的模型,只写这个工作区名下 */
|
|
26
|
+
export function writeRememberedHeroModel(storage: Pick<Storage, "setItem">, cwd: string, choice: HeroModelChoice): void {
|
|
27
|
+
storage.setItem(`${KEY_PREFIX}${cwd}`, JSON.stringify({ provider: choice.provider, id: choice.id }));
|
|
28
|
+
}
|