@rexvane/piweb 0.3.20 → 0.3.22
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 +6 -4
- 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 +12 -12
- package/.next/required-server-files.js +18 -2
- package/.next/required-server-files.json +18 -2
- package/.next/routes-manifest.json +12 -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 +3 -3
- 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]/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 +3 -3
- 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 +1 -1
- 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 +19 -13
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +25 -19
- package/.next/server/app/index.segments/_full.segment.rsc +19 -13
- package/.next/server/app/index.segments/_tree.segment.rsc +3 -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 +6 -4
- package/.next/server/chunks/229.js +1 -1
- package/.next/server/chunks/421.js +1 -0
- package/.next/server/chunks/430.js +215 -31
- package/.next/server/chunks/496.js +1 -1
- package/.next/server/chunks/538.js +1 -1
- package/.next/server/chunks/662.js +2 -0
- package/.next/server/chunks/686.js +1 -1
- package/.next/server/chunks/807.js +1 -1
- package/.next/server/chunks/813.js +23 -1
- package/.next/server/chunks/920.js +2 -2
- package/.next/server/chunks/924.js +1 -1
- package/.next/server/chunks/970.js +305 -0
- 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/static/WMazYvD-gbeLsCUfUxvvM/_buildManifest.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.4cf66cbdd8364c57.js +1 -0
- package/.next/static/chunks/4686-09fcd0028079006c.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.67366dc5032a141d.js +1 -0
- package/.next/static/chunks/8552.25c39ef4c995eba0.js +1 -0
- package/.next/static/chunks/app/_global-error/page-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/events/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/tree/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/agent/new/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/config/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/files/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/fs/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/git/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/github/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/growth/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/health/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/models/defaults/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/models/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/pi-settings/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/plugins/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/prompts/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/remote/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/security/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/export/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/sessions/import/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/sessions/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/skills/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/update/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/version/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/web-auth/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/api/workspaces/route-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/app/page-fc4ed2ff29b6235e.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-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f977af7f858990a2.js +1 -0
- package/.next/static/chunks/webpack-22ebffc20adb44ae.js +1 -0
- package/.next/static/css/6671d0d1589fdc0e.css +1 -0
- package/.next/static/css/{2959bd6b626b7ffe.css → f15a853af8b5127f.css} +1 -1
- 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]/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 +3 -1
- package/.next/types/validator.ts +76 -2
- package/README.md +14 -5
- package/next.config.ts +4 -2
- package/package.json +2 -2
- package/scripts/release.mjs +1 -1
- package/src/app/api/agent/[id]/events/route.ts +42 -13
- 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/api/update/route.ts +18 -2
- package/src/app/globals.css +275 -0
- package/src/components/AppShell.tsx +89 -33
- package/src/components/ChatInput.tsx +245 -63
- package/src/components/ChatWindow.tsx +123 -108
- package/src/components/ContextMeter.tsx +204 -123
- package/src/components/DrawerPanel.tsx +19 -26
- package/src/components/ExtensionUI.tsx +8 -2
- package/src/components/GitPanel.tsx +110 -40
- package/src/components/MarkdownBlock.tsx +333 -37
- 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 +365 -80
- package/src/components/TrajectoryView.tsx +4 -6
- package/src/components/chat/chat-scroll.ts +18 -0
- package/src/components/composer/ComposerImages.tsx +306 -0
- package/src/components/update/CopyCommand.tsx +34 -0
- package/src/components/update/UpdateNotice.tsx +130 -0
- package/src/components/update/update-store.ts +155 -0
- package/src/hooks/useGrowth.ts +3 -0
- package/src/hooks/usePiWeb.ts +54 -27
- package/src/hooks/useWorkspaceFiles.ts +93 -0
- package/src/i18n.tsx +182 -6
- package/src/instrumentation-node.ts +3 -0
- package/src/lib/agent-manager.ts +193 -119
- package/src/lib/chat/event-stream.ts +113 -0
- package/src/lib/chat/stream-settings.ts +192 -0
- package/src/lib/files-service.ts +100 -1
- 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/pi.ts +27 -2
- package/src/lib/process-format.ts +3 -2
- package/src/lib/session/metrics.ts +31 -0
- package/src/lib/thinking-level.ts +39 -0
- package/src/lib/trajectory.ts +17 -3
- package/src/lib/types.ts +19 -1
- package/src/lib/update/startup-check.ts +117 -0
- package/src/lib/update-service.ts +23 -3
- package/src/lib/workspace-index.ts +270 -0
- package/.next/server/chunks/131.js +0 -1
- package/.next/server/chunks/243.js +0 -1
- package/.next/server/chunks/741.js +0 -165
- package/.next/static/_Y3Lq0lI5RJ1tFSOnDi0L/_buildManifest.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-b1e9fa876d0ea671.js +0 -1
- package/.next/static/chunks/4778.e4c9ed093a413bc4.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.d3fe337a9117c325.js +0 -1
- package/.next/static/chunks/8552.6bfe3e101250b20f.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-2e425ef83fba0782.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-3614a8337843456b.js +0 -1
- package/.next/static/css/7391c42e1ae617f9.css +0 -1
- /package/.next/static/{_Y3Lq0lI5RJ1tFSOnDi0L → WMazYvD-gbeLsCUfUxvvM}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 思考档位的纯函数,客户端与服务端共用(不引入 pi-ai)。规则与 pi-ai 的 clampThinkingLevel、
|
|
3
|
+
* pi 的 createAgentSession 一致;tests/thinking-level.test.ts 里逐项和 pi-ai 对照。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** pi-ai 的 EXTENDED_THINKING_LEVELS:从低到高 */
|
|
7
|
+
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
8
|
+
|
|
9
|
+
/** pi 的 DEFAULT_THINKING_LEVEL:设置里没有默认档位、模型也没有覆盖时用它 */
|
|
10
|
+
export const DEFAULT_THINKING_LEVEL = "medium";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 把档位钳到模型支持的范围:支持就原样返回;否则先向更高的档位找,再向更低的档位找,都没有就取第一个支持的。
|
|
14
|
+
*/
|
|
15
|
+
export function clampThinkingLevelTo(supported: readonly string[], level: string): string {
|
|
16
|
+
if (supported.includes(level)) return level;
|
|
17
|
+
const index = THINKING_LEVELS.indexOf(level);
|
|
18
|
+
if (index === -1) return supported[0] ?? "off";
|
|
19
|
+
for (let i = index; i < THINKING_LEVELS.length; i += 1) {
|
|
20
|
+
if (supported.includes(THINKING_LEVELS[i])) return THINKING_LEVELS[i];
|
|
21
|
+
}
|
|
22
|
+
for (let i = index - 1; i >= 0; i -= 1) {
|
|
23
|
+
if (supported.includes(THINKING_LEVELS[i])) return THINKING_LEVELS[i];
|
|
24
|
+
}
|
|
25
|
+
return supported[0] ?? "off";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 新会话实际会用的档位(SDK createAgentSession 的规则):用户显式选过就用它;否则依次取该模型的每模型覆盖、
|
|
30
|
+
* 设置里的默认档位、DEFAULT_THINKING_LEVEL;最后按模型能力钳制。
|
|
31
|
+
*/
|
|
32
|
+
export function newSessionThinkingLevel(input: {
|
|
33
|
+
supported: readonly string[];
|
|
34
|
+
requested?: string;
|
|
35
|
+
modelOverride?: string;
|
|
36
|
+
defaultLevel?: string;
|
|
37
|
+
}): string {
|
|
38
|
+
return clampThinkingLevelTo(input.supported, input.requested || input.modelOverride || input.defaultLevel || DEFAULT_THINKING_LEVEL);
|
|
39
|
+
}
|
package/src/lib/trajectory.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 轨迹账本:对齐 dsh ui-trajectory 的 cell 模型(system/user/message/tool/compacted)。
|
|
3
3
|
* 活跃会话由 pi 事件实时驱动;冷会话从 JSONL 条目离线重建。
|
|
4
|
-
* pi
|
|
4
|
+
* pi 持久化 LLM / 工具 durationMs;TTFT/decode 由服务端自测,仅活跃会话有值。
|
|
5
5
|
*/
|
|
6
6
|
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import type { TrajEntry, TrajTokens } from "./types";
|
|
8
8
|
import { sanitizeToolOutput } from "./text-sanitize";
|
|
9
9
|
|
|
10
|
+
/** 只恢复 SDK 已记录的耗时,不从相邻消息时间戳推算。 */
|
|
11
|
+
function storedDuration(value: unknown): number | undefined {
|
|
12
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
export function toTrajTokens(u: any): TrajTokens | undefined {
|
|
11
16
|
if (!u || typeof u !== "object") return undefined;
|
|
12
17
|
const t: TrajTokens = {};
|
|
@@ -88,6 +93,8 @@ export class TrajLedger {
|
|
|
88
93
|
e.encodingLoss = result.encodingLoss || e.encodingLoss;
|
|
89
94
|
e.isError = m.isError === true;
|
|
90
95
|
e.tokens = toTrajTokens(m.usage) ?? e.tokens;
|
|
96
|
+
const durationMs = storedDuration(m.durationMs);
|
|
97
|
+
if (durationMs !== undefined) e.timing = { ...e.timing, durationMs };
|
|
91
98
|
this.touch(e);
|
|
92
99
|
}
|
|
93
100
|
}
|
|
@@ -96,7 +103,7 @@ export class TrajLedger {
|
|
|
96
103
|
case "message_update": {
|
|
97
104
|
const e: any = evt.assistantMessageEvent;
|
|
98
105
|
if (!e) break;
|
|
99
|
-
if ((e.type === "text_delta" || e.type === "thinking_delta") && this.streamingStart) {
|
|
106
|
+
if ((e.type === "text_delta" || e.type === "thinking_delta" || e.type === "toolcall_delta") && e.delta?.length > 0 && this.streamingStart) {
|
|
100
107
|
if (this.streamingStart.firstToken === undefined) {
|
|
101
108
|
this.streamingStart.firstToken = now;
|
|
102
109
|
const last = this.entries[this.entries.length - 1];
|
|
@@ -127,6 +134,8 @@ export class TrajLedger {
|
|
|
127
134
|
durationMs: now - this.streamingStart.t0,
|
|
128
135
|
};
|
|
129
136
|
}
|
|
137
|
+
const durationMs = storedDuration(m.durationMs);
|
|
138
|
+
if (durationMs !== undefined) e.timing = { ...e.timing, durationMs };
|
|
130
139
|
e.tokens = toTrajTokens(m.usage);
|
|
131
140
|
e.thinking = textOf(
|
|
132
141
|
(m.content ?? []).filter((c: any) => c.type === "thinking").map((c: any) => c.thinking),
|
|
@@ -194,7 +203,8 @@ export class TrajLedger {
|
|
|
194
203
|
e.encodingLoss = result.encodingLoss || e.encodingLoss;
|
|
195
204
|
e.isError = evt.isError === true;
|
|
196
205
|
const startedAt = this.toolStart.get(evt.toolCallId);
|
|
197
|
-
|
|
206
|
+
const durationMs = storedDuration((evt as any).durationMs) ?? (startedAt !== undefined ? now - startedAt : undefined);
|
|
207
|
+
if (durationMs !== undefined) e.timing = { ...(e.timing ?? {}), durationMs };
|
|
198
208
|
this.touch(e);
|
|
199
209
|
}
|
|
200
210
|
this.toolStart.delete(evt.toolCallId);
|
|
@@ -236,6 +246,7 @@ export function buildTrajectoryFromEntries(entries: any[]): TrajEntry[] {
|
|
|
236
246
|
} else if (m.role === "assistant") {
|
|
237
247
|
const thinking = textOf((m.content ?? []).filter((c: any) => c.type === "thinking"));
|
|
238
248
|
const text = textOf((m.content ?? []).filter((c: any) => c.type === "text"));
|
|
249
|
+
const durationMs = storedDuration(m.durationMs);
|
|
239
250
|
out.push({
|
|
240
251
|
seq: seq++,
|
|
241
252
|
kind: "message",
|
|
@@ -244,6 +255,7 @@ export function buildTrajectoryFromEntries(entries: any[]): TrajEntry[] {
|
|
|
244
255
|
detail: text,
|
|
245
256
|
thinking: thinking || undefined,
|
|
246
257
|
tokens: toTrajTokens(m.usage),
|
|
258
|
+
...(durationMs === undefined ? {} : { timing: { durationMs } }),
|
|
247
259
|
isError: m.stopReason === "error" || undefined,
|
|
248
260
|
});
|
|
249
261
|
for (const c of m.content ?? []) {
|
|
@@ -269,6 +281,8 @@ export function buildTrajectoryFromEntries(entries: any[]): TrajEntry[] {
|
|
|
269
281
|
target.encodingLoss = result.encodingLoss || target.encodingLoss;
|
|
270
282
|
target.isError = m.isError === true || undefined;
|
|
271
283
|
target.tokens = toTrajTokens(m.usage) ?? target.tokens;
|
|
284
|
+
const durationMs = storedDuration(m.durationMs);
|
|
285
|
+
if (durationMs !== undefined) target.timing = { durationMs };
|
|
272
286
|
}
|
|
273
287
|
}
|
|
274
288
|
} else if (raw.type === "compaction") {
|
package/src/lib/types.ts
CHANGED
|
@@ -116,6 +116,8 @@ export type WebEvent =
|
|
|
116
116
|
| { type: "traj"; entry: TrajEntry; ts: number }
|
|
117
117
|
| { type: "name"; name: string; ts: number }
|
|
118
118
|
| { type: "tools"; active: string[]; all: string[]; toolPreset?: ToolPreset; customActiveTools?: string[] | null; ts: number }
|
|
119
|
+
/** 上下文分项变了(会话启动、工具 / 资源重载、压缩之后):替换快照里的 contextBreakdown */
|
|
120
|
+
| { type: "context_breakdown"; breakdown: ContextBreakdown; ts: number }
|
|
119
121
|
| {
|
|
120
122
|
/** 资源(扩展/技能/模板/命令)重载后的新清单;前端据此刷新 / 菜单与设置面板 */
|
|
121
123
|
type: "resources";
|
|
@@ -143,8 +145,10 @@ export type WebEvent =
|
|
|
143
145
|
}
|
|
144
146
|
| { type: "extension_ui_resolved"; id: string; ts: number }
|
|
145
147
|
| { type: "error"; message: string; ts: number }
|
|
146
|
-
/**
|
|
148
|
+
/** 自动重试:流程内通知,界面聚合成一条「重试 n/max」,不作为错误条目累积 */
|
|
147
149
|
| { type: "retry"; attempt: number; maxAttempts: number; message: string; ts: number }
|
|
150
|
+
/** 重试区间结束(SDK auto_retry_end):收掉「重试 n/max」那一行;真失败由后续 error 消息表达 */
|
|
151
|
+
| { type: "retry_end"; attempt: number; ts: number }
|
|
148
152
|
/** 项目生长:提交了一个 commit(pi 的一轮 / 用户两轮之间的修改 / 工作区基线) */
|
|
149
153
|
| { type: "growth"; round: GrowthRound; ts: number }
|
|
150
154
|
| { type: "growth_error"; message: string | null; ts: number }
|
|
@@ -164,6 +168,11 @@ export interface WebStats {
|
|
|
164
168
|
ttftSteps: number;
|
|
165
169
|
decodeMs: number;
|
|
166
170
|
decodeTokens: number;
|
|
171
|
+
/** 有持久化耗时的消息 / 工具数量;旧历史缺数据时显示覆盖范围。 */
|
|
172
|
+
llmTimedSteps?: number;
|
|
173
|
+
toolTimedCalls?: number;
|
|
174
|
+
/** 生成速度实际测到的回复数,不代表整个历史。 */
|
|
175
|
+
decodeSteps?: number;
|
|
167
176
|
}
|
|
168
177
|
|
|
169
178
|
// ---------- 项目生长(每轮一个 git commit) ----------
|
|
@@ -234,6 +243,8 @@ export interface ContextResource {
|
|
|
234
243
|
* 这里只放服务端才能拿到的部分。
|
|
235
244
|
*/
|
|
236
245
|
export interface ContextBreakdown {
|
|
246
|
+
/** 当前投影中实际文本的字符数;不将估算 token 按比例分摊。 */
|
|
247
|
+
characters?: { systemPrompt: number; systemTools: number; customTools: number; memory: number; skills: number; compacted: number };
|
|
237
248
|
/** SDK 内建 system prompt 正文(不含 memory/skills/tools 段)的估算 token */
|
|
238
249
|
systemPrompt: number;
|
|
239
250
|
/** 内建工具(SDK 自带 bash/read/edit 等)定义的估算 token */
|
|
@@ -248,6 +259,11 @@ export interface ContextBreakdown {
|
|
|
248
259
|
compacted: number;
|
|
249
260
|
/** auto-compact 预留(compaction.reserveTokens 设置值;未启用为 0) */
|
|
250
261
|
autoCompactBuffer: number;
|
|
262
|
+
/**
|
|
263
|
+
* 会话还没启动(只浏览历史不冷启动 agent):systemPrompt / systemTools / customTools 拿不到、为 0,
|
|
264
|
+
* 其余几项照常统计。前端据此提示并提供「现在统计」(prepare 启动会话)。
|
|
265
|
+
*/
|
|
266
|
+
cold?: boolean;
|
|
251
267
|
}
|
|
252
268
|
|
|
253
269
|
/** SSE 连接建立时先发的完整快照 */
|
|
@@ -331,6 +347,8 @@ export interface AgentCommand {
|
|
|
331
347
|
instructions?: string;
|
|
332
348
|
provider?: string;
|
|
333
349
|
modelId?: string;
|
|
350
|
+
/** setModel:新会话页选的模型,思考档位按新会话的规则定(不沿用会话原来的档位) */
|
|
351
|
+
fresh?: boolean;
|
|
334
352
|
level?: string;
|
|
335
353
|
preset?: ToolPreset;
|
|
336
354
|
entryId?: string;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 启动时检查一次更新:PiWeb 和 pi 各查一次,结果留在内存里给设置页和右下角提示条用,
|
|
3
|
+
* 有新版本时在终端打一行。离线、registry 不通都不吭声——设置里还能手动检查。
|
|
4
|
+
*
|
|
5
|
+
* 状态挂 globalThis:instrumentation 和路由是不同的 bundle,模块级变量不共享(同 remote/supervisor)。
|
|
6
|
+
* 每次服务启动换一个 startId,页面据此做到「每次启动提醒一次」。
|
|
7
|
+
* 手动检查、更新成功后改写这份缓存:刷新页面不会又显示「更新到 vX」。
|
|
8
|
+
* PI_WEB_UPDATE_CHECK=0 关掉启动检查(不联网),设置里手动检查不受影响。
|
|
9
|
+
*/
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { checkForUpdate, type UpdateCheck, type UpdateTarget } from "../update-service";
|
|
12
|
+
|
|
13
|
+
/** 一项的结果:检查结果(更新过就带上 updatedTo),或检查失败的原因 */
|
|
14
|
+
export type StartupResult = (UpdateCheck & { updatedTo?: string }) | { error: string };
|
|
15
|
+
|
|
16
|
+
export interface StartupUpdates {
|
|
17
|
+
/** 本次服务启动的标识;关掉启动检查时为 null */
|
|
18
|
+
startId: string | null;
|
|
19
|
+
results: Partial<Record<UpdateTarget, StartupResult>>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface StartupState {
|
|
23
|
+
startId: string;
|
|
24
|
+
results: Partial<Record<UpdateTarget, StartupResult>>;
|
|
25
|
+
pending: Promise<void> | null;
|
|
26
|
+
started: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface StartupOptions {
|
|
30
|
+
check?: (target: UpdateTarget) => Promise<UpdateCheck>;
|
|
31
|
+
log?: (line: string) => void;
|
|
32
|
+
env?: Record<string, string | undefined>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const STATE_KEY = "piweb.update.startup";
|
|
36
|
+
const TARGETS: readonly UpdateTarget[] = ["piweb", "pi"];
|
|
37
|
+
const LABEL: Record<UpdateTarget, string> = { piweb: "PiWeb", pi: "Pi" };
|
|
38
|
+
|
|
39
|
+
function state(): StartupState {
|
|
40
|
+
const store = globalThis as Record<PropertyKey, unknown>;
|
|
41
|
+
const key = Symbol.for(STATE_KEY);
|
|
42
|
+
const existing = store[key] as StartupState | undefined;
|
|
43
|
+
if (existing) return existing;
|
|
44
|
+
const created: StartupState = { startId: randomUUID(), results: {}, pending: null, started: false };
|
|
45
|
+
store[key] = created;
|
|
46
|
+
return created;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function startupCheckEnabled(env: Record<string, string | undefined> = process.env): boolean {
|
|
50
|
+
return env.PI_WEB_UPDATE_CHECK !== "0";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 终端里那一行;没有新版本时为 null */
|
|
54
|
+
export function updateNoticeLine(results: StartupUpdates["results"]): string | null {
|
|
55
|
+
const parts: string[] = [];
|
|
56
|
+
let manual: string | undefined;
|
|
57
|
+
for (const target of TARGETS) {
|
|
58
|
+
const result = results[target];
|
|
59
|
+
if (!result || "error" in result || !result.canUpdate) continue;
|
|
60
|
+
// Git 检出版本号没变、只是 main 上有新提交时,写提交数而不是「0.3.20 → 0.3.20」
|
|
61
|
+
parts.push(result.latest === result.current && result.behind ? `${LABEL[target]}(main 新增 ${result.behind} 个提交)` : `${LABEL[target]} ${result.current} → ${result.latest}`);
|
|
62
|
+
if (!result.selfUpdate && result.command) manual = result.command;
|
|
63
|
+
}
|
|
64
|
+
if (!parts.length) return null;
|
|
65
|
+
const line = `[更新] 有新版本:${parts.join("、")}。打开 设置 → 通用 点「更新」`;
|
|
66
|
+
return manual ? `${line};PiWeb 要先停掉 piweb 再运行:${manual}` : line;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 启动检查(只跑一次);返回检查完之后的状态快照。关掉时返回 null,不联网。
|
|
71
|
+
* instrumentation 在启动时调它,GET /api/update 也调它(页面先到时由页面触发,结果一样只查一次)。
|
|
72
|
+
*/
|
|
73
|
+
export async function ensureStartupCheck({ check = checkForUpdate, log = console.log, env = process.env }: StartupOptions = {}): Promise<StartupUpdates | null> {
|
|
74
|
+
if (!startupCheckEnabled(env)) return null;
|
|
75
|
+
const current = state();
|
|
76
|
+
if (!current.started) {
|
|
77
|
+
current.started = true;
|
|
78
|
+
current.pending = (async () => {
|
|
79
|
+
await Promise.all(TARGETS.map(async (target) => {
|
|
80
|
+
try {
|
|
81
|
+
const result = await check(target);
|
|
82
|
+
// 检查期间已经手动更新过的项不覆盖
|
|
83
|
+
if (!current.results[target]) current.results[target] = result;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (!current.results[target]) current.results[target] = { error: error instanceof Error ? error.message : String(error) };
|
|
86
|
+
}
|
|
87
|
+
}));
|
|
88
|
+
const line = updateNoticeLine(current.results);
|
|
89
|
+
if (line) log(line);
|
|
90
|
+
})();
|
|
91
|
+
}
|
|
92
|
+
await current.pending;
|
|
93
|
+
return snapshot();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function snapshot(): StartupUpdates {
|
|
97
|
+
const current = state();
|
|
98
|
+
return { startId: current.startId, results: { ...current.results } };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 手动检查的结果覆盖缓存 */
|
|
102
|
+
export function recordCheck(target: UpdateTarget, check: UpdateCheck): void {
|
|
103
|
+
state().results[target] = check;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 更新成功:记下装好的版本,页面刷新后显示「已更新,重启生效」而不是又让人更新一次 */
|
|
107
|
+
export function markUpdated(target: UpdateTarget, version: string): void {
|
|
108
|
+
const current = state();
|
|
109
|
+
const previous = current.results[target];
|
|
110
|
+
const base = previous && !("error" in previous) ? previous : { current: "", latest: version, canUpdate: false, selfUpdate: true };
|
|
111
|
+
current.results[target] = { ...base, latest: version || base.latest, canUpdate: false, updatedTo: version || base.latest };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 仅供测试:清掉挂在 globalThis 上的状态 */
|
|
115
|
+
export function resetStartupCheck(): void {
|
|
116
|
+
delete (globalThis as Record<PropertyKey, unknown>)[Symbol.for(STATE_KEY)];
|
|
117
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import fs from "node:fs/promises";
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import { installPiUpdate, PI_PACKAGE, prepareRelease } from "../../scripts/release.mjs";
|
|
8
|
+
import { installPiUpdate, PI_AI_PACKAGE, PI_PACKAGE, prepareRelease } from "../../scripts/release.mjs";
|
|
9
9
|
import { assertInstallation, readProductionBuild } from "../../scripts/build-output.mjs";
|
|
10
10
|
import { runCommand } from "../../scripts/process-runner.mjs";
|
|
11
11
|
import { isNewer } from "./semver";
|
|
@@ -153,14 +153,34 @@ export function createUpdateService({ root = APP_ROOT, run = runCommand, version
|
|
|
153
153
|
return { ...readProductionBuild(installation, ".next"), version, needsRestart: true };
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
/**
|
|
157
|
+
* 只换 npm 成品包里的 pi 依赖:成品包没有 devDependencies,跑不了 typecheck / 构建,也不需要——
|
|
158
|
+
* pi 两个包都在 serverExternalPackages 里,运行时从这里的 node_modules 加载,重启即用上新版。
|
|
159
|
+
* Windows 也能做:运行中的服务没加载 pi 的原生模块,pi 目录也不是服务的工作目录,不会 EBUSY。
|
|
160
|
+
* 图标不用同步:成品包自带 public/provider-icons,清单也和 pi 版本无关。
|
|
161
|
+
*/
|
|
162
|
+
async function installPiIntoPackage(installation: string, assertIdle: () => Promise<void>): Promise<UpdateResult> {
|
|
163
|
+
await assertIdle();
|
|
164
|
+
await run("npm", ["install", "--save-exact", `${PI_PACKAGE}@latest`, `${PI_AI_PACKAGE}@latest`, "--omit=dev", "--no-audit", "--no-fund"], {
|
|
165
|
+
cwd: installation, env: commandEnv, timeoutMs: 600_000,
|
|
166
|
+
});
|
|
167
|
+
await assertIdle();
|
|
168
|
+
assertInstallation(installation);
|
|
169
|
+
const version = await readPackageVersion(path.join(installation, "node_modules", PI_PACKAGE, "package.json"));
|
|
170
|
+
return { ...readProductionBuild(installation, ".next"), version, needsRestart: true };
|
|
171
|
+
}
|
|
172
|
+
|
|
156
173
|
async function runUpdate(target: UpdateTarget, { assertIdle = async () => {} }: { assertIdle?: () => Promise<void> } = {}): Promise<UpdateResult> {
|
|
157
174
|
assertTarget(target);
|
|
158
175
|
if (updateRunning) throw new UpdateBusyError("an update is already running");
|
|
159
176
|
updateRunning = true;
|
|
160
177
|
try {
|
|
161
178
|
const installation = await checkedRoot();
|
|
162
|
-
// npm 安装:装发布好的成品即可——不需要 Git,也不需要本地构建(成品包自带 .next
|
|
163
|
-
|
|
179
|
+
// npm 安装:装发布好的成品即可——不需要 Git,也不需要本地构建(成品包自带 .next);
|
|
180
|
+
// 单独更新 pi 只换依赖,同样不重建。
|
|
181
|
+
if (isNpmInstall(installation)) {
|
|
182
|
+
return target === "piweb" ? await installFromRegistry(installation, assertIdle) : await installPiIntoPackage(installation, assertIdle);
|
|
183
|
+
}
|
|
164
184
|
let version = "";
|
|
165
185
|
const release = await prepareRelease({
|
|
166
186
|
root: installation, env, run,
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ 引用的候选与匹配(纯逻辑,便于单测):照搬 pi 终端的 `@` 补全
|
|
3
|
+
* (pi-tui `CombinedAutocompleteProvider.getFuzzyFileSuggestions` + `extractAtPrefix`),
|
|
4
|
+
* 同一个 `@que` 在终端和网页里给出同一批、同一顺序的候选。
|
|
5
|
+
*
|
|
6
|
+
* pi 的做法:`@` 后的文字按最后一个 `/` 拆成「目录 + 查询」(目录存在才算),
|
|
7
|
+
* 在该目录下用 fd 过滤(查询当正则、smart case,默认只匹配文件名,带 `/` 时匹配整条路径),
|
|
8
|
+
* 再用 scoreEntry 打分(文件名全等 100 / 前缀 80 / 包含 50 / 路径包含 30,目录 +10),
|
|
9
|
+
* 按分数、层级、路径长度、字典序排,取前 20。
|
|
10
|
+
*
|
|
11
|
+
* 有意的差别(tests/mention-parity.test.ts 里逐条对照,差别也写在那里):
|
|
12
|
+
* - 候选来自服务端给的工作区清单(git 可见文件 + 推导出的文件夹),不现场跑 fd;
|
|
13
|
+
* pi 每次 fd 只取前 100 条再打分,这里对整份清单打分——结果是「fd 不截断时 pi 会给的」,而且稳定。
|
|
14
|
+
* - 不支持 `~/`、绝对路径和跳出工作区的 `../`:引用只针对当前工作区。
|
|
15
|
+
* - fd 的 `--full-path` 匹配绝对路径,这里匹配相对路径(只差在工作区自身路径恰好命中的极端情况)。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export interface PathCandidate {
|
|
19
|
+
/** 工作区根的相对路径("/" 分隔),目录不带尾斜杠 */
|
|
20
|
+
path: string;
|
|
21
|
+
dir: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 显示用:目录带尾斜杠 */
|
|
25
|
+
export function candidateText(candidate: PathCandidate): string {
|
|
26
|
+
return candidate.dir ? `${candidate.path}/` : candidate.path;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 文件路径 → 全部父目录(去重、不含 ".") */
|
|
30
|
+
export function dirsOfPaths(files: string[]): string[] {
|
|
31
|
+
const dirs = new Set<string>();
|
|
32
|
+
for (const file of files) {
|
|
33
|
+
const parts = file.split("/");
|
|
34
|
+
for (let i = 1; i < parts.length; i += 1) dirs.add(parts.slice(0, i).join("/"));
|
|
35
|
+
}
|
|
36
|
+
return [...dirs];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function buildCandidates(files: string[], dirs: string[]): PathCandidate[] {
|
|
40
|
+
const out: PathCandidate[] = [];
|
|
41
|
+
for (const dir of dirs) out.push({ path: dir, dir: true });
|
|
42
|
+
for (const file of files) out.push({ path: file, dir: false });
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------- 匹配与排序(pi-tui autocomplete.js) ----------
|
|
47
|
+
|
|
48
|
+
/** pi 一次最多列 20 条 */
|
|
49
|
+
export const MENTION_LIMIT = 20;
|
|
50
|
+
|
|
51
|
+
export interface MentionSuggestion {
|
|
52
|
+
candidate: PathCandidate;
|
|
53
|
+
/** 文件名,目录带尾斜杠(pi 的 label) */
|
|
54
|
+
label: string;
|
|
55
|
+
/** 路径(pi 的 description):按用户打的目录前缀显示,如 `./src/lib` */
|
|
56
|
+
description: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function toDisplayPath(value: string): string {
|
|
60
|
+
return value.replace(/\\/g, "/");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function baseName(path: string): string {
|
|
64
|
+
const trimmed = path.replace(/\/+$/, "");
|
|
65
|
+
return trimmed.slice(trimmed.lastIndexOf("/") + 1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function escapeRegex(value: string): string {
|
|
69
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** pi 的 buildFdPathQuery:带 `/` 的查询按段转义、段间允许 `/` 或 `\` */
|
|
73
|
+
function buildFdPathQuery(query: string): string {
|
|
74
|
+
const normalized = toDisplayPath(query);
|
|
75
|
+
if (!normalized.includes("/")) return normalized;
|
|
76
|
+
const trimmed = normalized.replace(/^\/+|\/+$/g, "");
|
|
77
|
+
if (!trimmed) return normalized;
|
|
78
|
+
const segments = trimmed.split("/").filter(Boolean).map(escapeRegex);
|
|
79
|
+
if (!segments.length) return normalized;
|
|
80
|
+
const separator = "[\\\\/]";
|
|
81
|
+
return segments.join(separator) + (normalized.endsWith("/") ? separator : "");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* fd 的过滤:模式当正则,smart case(模式里有大写字母才区分大小写,转义序列如 `\W` 不算)。
|
|
86
|
+
* fd 用的是 Rust 正则,`c++` 这种叠加量词它能编(等于 `(c+)+`),JS 会报错:
|
|
87
|
+
* 编不过就退回字面包含——之后 scoreEntry 本来就要求查询按字面出现,所以 `c++` 的结果和 pi 一样;
|
|
88
|
+
* Rust 也编不过的(未闭合的括号等)pi 没有候选,这里按字面给,算有意放宽。
|
|
89
|
+
*/
|
|
90
|
+
function fdFilter(pattern: string): (target: string) => boolean {
|
|
91
|
+
const caseSensitive = /\p{Lu}/u.test(pattern.replace(/\\./g, ""));
|
|
92
|
+
try {
|
|
93
|
+
const regex = new RegExp(pattern, caseSensitive ? "" : "i");
|
|
94
|
+
return (target) => regex.test(target);
|
|
95
|
+
} catch {
|
|
96
|
+
const literal = caseSensitive ? pattern : pattern.toLowerCase();
|
|
97
|
+
return (target) => (caseSensitive ? target : target.toLowerCase()).includes(literal);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** pi 的 scoreEntry:文件名全等 100 / 前缀 80 / 包含 50 / 整条路径包含 30,目录命中再 +10 */
|
|
102
|
+
export function scoreEntry(filePath: string, query: string, isDirectory: boolean): number {
|
|
103
|
+
const lowerFileName = baseName(filePath).toLowerCase();
|
|
104
|
+
const lowerQuery = query.toLowerCase();
|
|
105
|
+
let score = 0;
|
|
106
|
+
if (lowerFileName === lowerQuery) score = 100;
|
|
107
|
+
else if (lowerFileName.startsWith(lowerQuery)) score = 80;
|
|
108
|
+
else if (lowerFileName.includes(lowerQuery)) score = 50;
|
|
109
|
+
else if (filePath.toLowerCase().includes(lowerQuery)) score = 30;
|
|
110
|
+
if (isDirectory && score > 0) score += 10;
|
|
111
|
+
return score;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* pi 的 resolveScopedFuzzyQuery:最后一个 `/` 之前是目录(必须存在),之后是查询。
|
|
116
|
+
* 返回 base = 工作区相对路径("" 为根),displayBase = 用户打的原样前缀。
|
|
117
|
+
*/
|
|
118
|
+
function resolveScopedQuery(rawQuery: string, dirs: ReadonlySet<string>): { base: string; query: string; displayBase: string } | null {
|
|
119
|
+
const normalized = toDisplayPath(rawQuery);
|
|
120
|
+
const slash = normalized.lastIndexOf("/");
|
|
121
|
+
if (slash === -1) return null;
|
|
122
|
+
const displayBase = normalized.slice(0, slash + 1);
|
|
123
|
+
if (displayBase.startsWith("/") || displayBase.startsWith("~/")) return null;
|
|
124
|
+
const segments: string[] = [];
|
|
125
|
+
for (const segment of displayBase.split("/")) {
|
|
126
|
+
if (!segment || segment === ".") continue;
|
|
127
|
+
if (segment === "..") {
|
|
128
|
+
if (!segments.length) return null;
|
|
129
|
+
segments.pop();
|
|
130
|
+
} else segments.push(segment);
|
|
131
|
+
}
|
|
132
|
+
const base = segments.join("/");
|
|
133
|
+
if (base && !dirs.has(base)) return null;
|
|
134
|
+
return { base, query: normalized.slice(slash + 1), displayBase };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface Entry {
|
|
138
|
+
candidate: PathCandidate;
|
|
139
|
+
/** 相对 fd 搜索目录的路径,目录带尾斜杠(和 fd 的输出一致,长度和字典序都按它算) */
|
|
140
|
+
path: string;
|
|
141
|
+
score: number;
|
|
142
|
+
depth: number;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 候选排序与截断,和 pi 一致:空查询全部 1 分,按层级 → 路径长度 → 字典序(先看当前目录、名字短的在前)。
|
|
147
|
+
* 不在模块里缓存目录集合:调用方用 useMemo 包住整次计算。
|
|
148
|
+
*/
|
|
149
|
+
export function suggestPaths(candidates: readonly PathCandidate[], rawQuery: string, limit = MENTION_LIMIT): MentionSuggestion[] {
|
|
150
|
+
const dirs = new Set<string>();
|
|
151
|
+
for (const candidate of candidates) if (candidate.dir) dirs.add(candidate.path);
|
|
152
|
+
const scoped = resolveScopedQuery(rawQuery, dirs);
|
|
153
|
+
const query = scoped ? scoped.query : rawQuery;
|
|
154
|
+
const prefix = scoped?.base ? `${scoped.base}/` : "";
|
|
155
|
+
|
|
156
|
+
let filter: ((relative: string) => boolean) | null = null;
|
|
157
|
+
if (query) {
|
|
158
|
+
// 默认只拿文件名去匹配;查询带 `/` 时(目录不存在、没走上面的限定目录)匹配整条路径
|
|
159
|
+
const fullPath = toDisplayPath(query).includes("/");
|
|
160
|
+
const matches = fdFilter(buildFdPathQuery(query));
|
|
161
|
+
filter = fullPath ? matches : (relative) => matches(baseName(relative));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const entries: Entry[] = [];
|
|
165
|
+
for (const candidate of candidates) {
|
|
166
|
+
if (prefix && !candidate.path.startsWith(prefix)) continue;
|
|
167
|
+
const relative = candidate.path.slice(prefix.length);
|
|
168
|
+
if (!relative || (filter && !filter(relative))) continue;
|
|
169
|
+
const path = candidate.dir ? `${relative}/` : relative;
|
|
170
|
+
const score = query ? scoreEntry(path, query, candidate.dir) : 1;
|
|
171
|
+
if (score <= 0) continue;
|
|
172
|
+
entries.push({ candidate, path, score, depth: relative.split("/").filter(Boolean).length });
|
|
173
|
+
}
|
|
174
|
+
entries.sort((a, b) => b.score - a.score || a.depth - b.depth || a.path.length - b.path.length || a.path.localeCompare(b.path));
|
|
175
|
+
|
|
176
|
+
return entries.slice(0, limit).map(({ candidate, path }) => {
|
|
177
|
+
const withoutSlash = candidate.dir ? path.slice(0, -1) : path;
|
|
178
|
+
return {
|
|
179
|
+
candidate,
|
|
180
|
+
label: baseName(withoutSlash) + (candidate.dir ? "/" : ""),
|
|
181
|
+
description: scoped ? `${scoped.displayBase}${withoutSlash}` : withoutSlash,
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---------- @ token(pi-tui extractAtPrefix) ----------
|
|
187
|
+
|
|
188
|
+
/** pi 的 PATH_DELIMITERS */
|
|
189
|
+
const PATH_DELIMITERS = new Set([" ", "\t", '"', "'", "="]);
|
|
190
|
+
/** pi-tui utils.js 的 cjkBreakRegex / cjkPunctuationRegex / autocompleteSeparatorRegex(原样抄来,测试里核对 source) */
|
|
191
|
+
export const CJK_BREAK_REGEX = /[\p{Script_Extensions=Han}\p{Script_Extensions=Hiragana}\p{Script_Extensions=Katakana}\p{Script_Extensions=Hangul}\p{Script_Extensions=Bopomofo}]/u;
|
|
192
|
+
const CJK_PUNCTUATION_REGEX = new RegExp(`(?:(?=\\p{Punctuation})${CJK_BREAK_REGEX.source}|[,.:;!?()[]{}“”‘’…—])`, "u");
|
|
193
|
+
export const AUTOCOMPLETE_SEPARATOR_REGEX = new RegExp(`(?:\\s|${CJK_PUNCTUATION_REGEX.source})`, "u");
|
|
194
|
+
const TOKEN_START_REGEX = new RegExp(`(?:^|${AUTOCOMPLETE_SEPARATOR_REGEX.source})$`, "u");
|
|
195
|
+
/** 正文里路径前常见的左括号类:`(@src` 也能触发;token 里有对应的右括号就保留(`app/[slug]`) */
|
|
196
|
+
const PATH_WRAPPERS: Record<string, string> = { "(": ")", "[": "]", "{": "}", "<": ">", "`": "`" };
|
|
197
|
+
|
|
198
|
+
function isDelimiter(character: string): boolean {
|
|
199
|
+
return PATH_DELIMITERS.has(character) || AUTOCOMPLETE_SEPARATOR_REGEX.test(character);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** 最后一个分隔符的位置(按码点走,返回它最后一个 UTF-16 单元的下标),没有则 -1 */
|
|
203
|
+
function findLastDelimiter(text: string): number {
|
|
204
|
+
let last = -1;
|
|
205
|
+
let index = 0;
|
|
206
|
+
for (const character of text) {
|
|
207
|
+
index += character.length;
|
|
208
|
+
if (isDelimiter(character)) last = index - 1;
|
|
209
|
+
}
|
|
210
|
+
return last;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function stripLeadingWrappers(token: string): string {
|
|
214
|
+
let result = token;
|
|
215
|
+
while (result.length > 0) {
|
|
216
|
+
const closer = PATH_WRAPPERS[result[0]];
|
|
217
|
+
if (!closer || result.includes(closer, 1)) break;
|
|
218
|
+
result = result.slice(1);
|
|
219
|
+
}
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function findUnclosedQuoteStart(text: string): number | null {
|
|
224
|
+
let inQuotes = false;
|
|
225
|
+
let quoteStart = -1;
|
|
226
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
227
|
+
if (text[i] === '"') {
|
|
228
|
+
inQuotes = !inQuotes;
|
|
229
|
+
if (inQuotes) quoteStart = i;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return inQuotes ? quoteStart : null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function isTokenStart(text: string, index: number): boolean {
|
|
236
|
+
let start = index;
|
|
237
|
+
while (start > 0 && PATH_WRAPPERS[text[start - 1]]) start -= 1;
|
|
238
|
+
return PATH_DELIMITERS.has(text[start - 1] ?? "") || TOKEN_START_REGEX.test(text.slice(0, start));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* 光标前的 `@` 引用 token,规则同 pi:
|
|
243
|
+
* 从最后一个分隔符(空白、`"` `'` `=`、中日韩标点)之后取一段,去掉开头的左括号类,以 `@` 开头才算
|
|
244
|
+
* (`foo@bar` 这种邮箱不触发);另支持 `@"带空格 的路径`。返回 null 表示当前不该弹候选。
|
|
245
|
+
* pi 只看光标所在的那一行;光标在 token 中间时(`@lib|bar`)删除范围盖住整个 token,免得留下半截。
|
|
246
|
+
*/
|
|
247
|
+
export function atToken(text: string, caret: number): { query: string; start: number; end: number; tokenEnd: number } | null {
|
|
248
|
+
const end = Math.max(0, Math.min(caret, text.length));
|
|
249
|
+
const lineStart = text.lastIndexOf("\n", end - 1) + 1;
|
|
250
|
+
const line = text.slice(lineStart, end);
|
|
251
|
+
|
|
252
|
+
const quoteStart = findUnclosedQuoteStart(line);
|
|
253
|
+
if (quoteStart !== null && quoteStart > 0 && line[quoteStart - 1] === "@" && isTokenStart(line, quoteStart - 1)) {
|
|
254
|
+
const close = text.indexOf('"', end);
|
|
255
|
+
const lineEnd = text.indexOf("\n", end);
|
|
256
|
+
const closed = close !== -1 && (lineEnd === -1 || close < lineEnd);
|
|
257
|
+
return { query: line.slice(quoteStart + 1), start: lineStart + quoteStart - 1, end, tokenEnd: closed ? close + 1 : end };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const tokenStart = findLastDelimiter(line) + 1;
|
|
261
|
+
const raw = line.slice(tokenStart);
|
|
262
|
+
const token = stripLeadingWrappers(raw);
|
|
263
|
+
if (!token.startsWith("@")) return null;
|
|
264
|
+
let tokenEnd = end;
|
|
265
|
+
for (const character of text.slice(end)) {
|
|
266
|
+
if (isDelimiter(character)) break;
|
|
267
|
+
tokenEnd += character.length;
|
|
268
|
+
}
|
|
269
|
+
return { query: token.slice(1), start: lineStart + tokenStart + raw.length - token.length, end, tokenEnd };
|
|
270
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"use strict";exports.id=131,exports.ids=[131],exports.modules={6512:(a,b,c)=>{c.d(b,{Kt:()=>f,W8:()=>e});let d=["off","minimal","low","medium","high","xhigh","max"];function e(a){return a.reasoning?d.filter(b=>{let c=a.thinkingLevelMap?.[b];return null!==c&&("xhigh"!==b&&"max"!==b||void 0!==c)}):["off"]}function f(a,b){let c=e(a);if(c.includes(b))return b;let f=d.indexOf(b);if(-1===f)return c[0]??"off";for(let a=f;a<d.length;a++){let b=d[a];if(c.includes(b))return b}for(let a=f-1;a>=0;a--){let b=d[a];if(c.includes(b))return b}return c[0]??"off"}}};
|