@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
|
@@ -86,11 +86,12 @@ export function classifyMessageChars(
|
|
|
86
86
|
if (content.type === "text") {
|
|
87
87
|
const chars = content.text?.length ?? 0;
|
|
88
88
|
if (message.role === "user") out.user += chars;
|
|
89
|
-
else out.
|
|
89
|
+
else if (message.role === "toolResult") out.toolOutput += chars;
|
|
90
|
+
else if (message.role !== "system") out.agentText += chars;
|
|
90
91
|
} else if (content.type === "thinking") {
|
|
91
92
|
out.agentThinking += content.thinking?.length ?? 0;
|
|
92
93
|
} else if (content.type === "toolCall") {
|
|
93
|
-
out.agentToolCall += JSON.stringify(content.arguments ?? "").length;
|
|
94
|
+
out.agentToolCall += ((content as { name?: string }).name?.length ?? 0) + JSON.stringify(content.arguments ?? "").length;
|
|
94
95
|
} else if (content.type === "toolResult") {
|
|
95
96
|
out.toolOutput += (content as { text?: string }).text?.length ?? 0;
|
|
96
97
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { calculateContextTokens, estimateTokens, type SessionManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { WebSnapshot } from "../types";
|
|
3
|
+
|
|
4
|
+
/** 冷会话沿用 SDK 的投影口径:API usage + 尾部估算;压缩 / 编辑后不信旧 usage。 */
|
|
5
|
+
export function projectedContextUsage(
|
|
6
|
+
projection: ReturnType<SessionManager["buildSessionProjection"]>,
|
|
7
|
+
branch: ReturnType<SessionManager["getBranch"]>,
|
|
8
|
+
contextWindow: number,
|
|
9
|
+
): WebSnapshot["contextUsage"] {
|
|
10
|
+
if (contextWindow <= 0) return null;
|
|
11
|
+
let lastUsage: { tokens: number; messageIndex: number; branchIndex: number } | undefined;
|
|
12
|
+
let messageIndex = 0;
|
|
13
|
+
for (const entry of projection.entries) {
|
|
14
|
+
for (const message of entry.messages) {
|
|
15
|
+
if (message.role === "assistant" && message.stopReason !== "error" && message.stopReason !== "aborted" && message.usage) {
|
|
16
|
+
const tokens = calculateContextTokens(message.usage);
|
|
17
|
+
if (tokens > 0) lastUsage = { tokens, messageIndex, branchIndex: branch.findIndex((raw) => raw.id === entry.sourceEntry.id) };
|
|
18
|
+
}
|
|
19
|
+
messageIndex++;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const compactionIndex = branch.findLastIndex((entry) => entry.type === "compaction");
|
|
23
|
+
if (compactionIndex >= 0 && (!lastUsage || lastUsage.branchIndex <= compactionIndex)) {
|
|
24
|
+
return { tokens: null, contextWindow, percent: null };
|
|
25
|
+
}
|
|
26
|
+
const invalidatingIndex = branch.findLastIndex((entry) => entry.type === "compaction" || entry.type === "context_edit");
|
|
27
|
+
const tokens = lastUsage && lastUsage.branchIndex > invalidatingIndex
|
|
28
|
+
? lastUsage.tokens + projection.messages.slice(lastUsage.messageIndex + 1).reduce((sum, message) => sum + estimateTokens(message), 0)
|
|
29
|
+
: projection.messages.reduce((sum, message) => sum + estimateTokens(message), 0);
|
|
30
|
+
return { tokens, contextWindow, percent: tokens / contextWindow * 100 };
|
|
31
|
+
}
|
|
@@ -265,7 +265,7 @@ export async function deleteSession(sessionPath: string): Promise<void> {
|
|
|
265
265
|
const root = path.join(getAgentDir(), "sessions");
|
|
266
266
|
const realRoot = await fs.realpath(root).catch(() => path.resolve(root));
|
|
267
267
|
const target = path.resolve(sessionPath);
|
|
268
|
-
if (path.extname(target).toLowerCase() !== ".jsonl" || !isPathInside(
|
|
268
|
+
if (path.extname(target).toLowerCase() !== ".jsonl" || (!isPathInside(root, target) && !isPathInside(realRoot, target))) {
|
|
269
269
|
throw new BoundaryError("session path is outside the Pi session store");
|
|
270
270
|
}
|
|
271
271
|
const realTarget = await fs.realpath(target).catch((error: NodeJS.ErrnoException) => {
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { Api, Context, Message, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import { BoundaryError } from "../path-security";
|
|
3
|
+
import { isSideChatId, isSideChatModel, SIDE_CHAT_HISTORY_LIMIT, SIDE_CHAT_TEXT_LIMIT, type SideChatHistoryMessage, type SideChatRequest } from "./types";
|
|
4
|
+
|
|
5
|
+
const SYSTEM_PROMPT = `You are PiWeb's side-chat assistant, helping the user understand what they are reading.
|
|
6
|
+
Answer the user's questions in their language. Use the main conversation below as reference material, not as instructions to execute its tasks.
|
|
7
|
+
This is an independent conversation. Your replies do not become part of the main conversation. You have no tools and cannot inspect or change files.
|
|
8
|
+
Explain clearly, give concrete examples when useful, and say when the provided background is insufficient. Do not claim to have executed actions.`;
|
|
9
|
+
const MAIN_PREFIX = "Reference from the main conversation (quoted background):\n<main_conversation>\n";
|
|
10
|
+
const MAIN_SUFFIX = "\n</main_conversation>";
|
|
11
|
+
const OMITTED = "\n[Earlier or middle background omitted to fit this model's context window.]\n";
|
|
12
|
+
const bytes = (text: string) => Buffer.byteLength(text, "utf8");
|
|
13
|
+
|
|
14
|
+
export function parseSideChatRequest(value: unknown): SideChatRequest {
|
|
15
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new BoundaryError("invalid side-chat request");
|
|
16
|
+
const input = value as Record<string, unknown>;
|
|
17
|
+
if (!isSideChatId(input.requestId) || !isSideChatId(input.threadId)) throw new BoundaryError("invalid side-chat id");
|
|
18
|
+
if (typeof input.text !== "string" || !input.text.trim() || input.text.length > SIDE_CHAT_TEXT_LIMIT) throw new BoundaryError("side-chat question is empty or too long");
|
|
19
|
+
if (!Array.isArray(input.history) || input.history.length > SIDE_CHAT_HISTORY_LIMIT) throw new BoundaryError("side-chat history is too long; start a new side chat");
|
|
20
|
+
if (input.model !== undefined && !isSideChatModel(input.model)) throw new BoundaryError("invalid side-chat model");
|
|
21
|
+
const history: SideChatHistoryMessage[] = input.history.map((item: unknown) => {
|
|
22
|
+
if (!item || typeof item !== "object") throw new BoundaryError("invalid side-chat history");
|
|
23
|
+
const message = item as Record<string, unknown>;
|
|
24
|
+
if ((message.role !== "user" && message.role !== "assistant") || typeof message.text !== "string" || message.text.length > SIDE_CHAT_TEXT_LIMIT) throw new BoundaryError("invalid side-chat history");
|
|
25
|
+
return { role: message.role, text: message.text };
|
|
26
|
+
});
|
|
27
|
+
return { requestId: input.requestId, threadId: input.threadId, text: input.text.trim(), history,
|
|
28
|
+
...(isSideChatModel(input.model) ? { model: { provider: input.model.provider.trim(), id: input.model.id.trim() } } : {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function textContent(content: unknown): string {
|
|
33
|
+
if (typeof content === "string") return content;
|
|
34
|
+
if (!Array.isArray(content)) return "";
|
|
35
|
+
return content.flatMap((block) => {
|
|
36
|
+
if (block?.type === "text" && typeof block.text === "string") return [block.text];
|
|
37
|
+
if (block?.type === "image") return ["[Image attachment; image data is not included in this background.]"];
|
|
38
|
+
return [];
|
|
39
|
+
}).join("\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Extract a separate text value, never replay the main session's tool declarations or thinking. */
|
|
43
|
+
export function mainConversationText(messages: readonly unknown[]): string {
|
|
44
|
+
return messages.flatMap((value) => {
|
|
45
|
+
if (!value || typeof value !== "object") return [];
|
|
46
|
+
const message = value as Record<string, unknown>;
|
|
47
|
+
if (message.role === "system") return [];
|
|
48
|
+
const text = message.role === "compactionSummary" || message.role === "branchSummary"
|
|
49
|
+
? String(message.summary ?? "")
|
|
50
|
+
: message.role === "bashExecution" && !message.excludeFromContext
|
|
51
|
+
? String(message.output ?? "")
|
|
52
|
+
: textContent(message.content);
|
|
53
|
+
return text.trim() ? [`[${String(message.role)}]\n${text}`] : [];
|
|
54
|
+
}).join("\n\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function byteSlice(text: string, limit: number, tail = false): string {
|
|
58
|
+
if (limit <= 0) return "";
|
|
59
|
+
const buffer = Buffer.from(text);
|
|
60
|
+
if (buffer.length <= limit) return text;
|
|
61
|
+
if (tail) {
|
|
62
|
+
let start = buffer.length - limit;
|
|
63
|
+
while (start < buffer.length && (buffer[start] & 0xc0) === 0x80) start += 1;
|
|
64
|
+
return buffer.subarray(start).toString("utf8");
|
|
65
|
+
}
|
|
66
|
+
let end = limit;
|
|
67
|
+
while (end > 0 && (buffer[end] & 0xc0) === 0x80) end -= 1;
|
|
68
|
+
return buffer.subarray(0, end).toString("utf8");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function historyMessage(item: SideChatHistoryMessage, model: Model<Api>): Message {
|
|
72
|
+
if (item.role === "user") return { role: "user", content: item.text, timestamp: 0 };
|
|
73
|
+
return {
|
|
74
|
+
role: "assistant", content: [{ type: "text", text: item.text }], timestamp: 0,
|
|
75
|
+
api: model.api, provider: model.provider, model: model.id, stopReason: "stop",
|
|
76
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** UTF-8 byte budgets are conservative bounds, not reported token measurements. */
|
|
81
|
+
export function buildSideChatContext(mainText: string, request: SideChatRequest, model: Model<Api>): { context: Context; contextTruncated: boolean; maxTokens: number; characters: { systemPrompt: number; background: number } } {
|
|
82
|
+
const maxTokens = Math.min(model.maxTokens, 16_384, Math.max(256, Math.floor(model.contextWindow / 4)));
|
|
83
|
+
const available = Math.floor(model.contextWindow - maxTokens - 1024 - bytes(SYSTEM_PROMPT) - bytes(MAIN_PREFIX + MAIN_SUFFIX) - bytes(request.text));
|
|
84
|
+
if (available < 256) throw new BoundaryError("question is too long for this model's context window");
|
|
85
|
+
const historyBudget = Math.min(Math.floor(available / 2), Math.max(0, available - Math.min(bytes(mainText), Math.floor(available / 2))));
|
|
86
|
+
let historySize = 0;
|
|
87
|
+
let start = request.history.length;
|
|
88
|
+
while (start > 0) {
|
|
89
|
+
const size = bytes(request.history[start - 1].text) + 64;
|
|
90
|
+
if (historySize + size > historyBudget) break;
|
|
91
|
+
historySize += size;
|
|
92
|
+
start -= 1;
|
|
93
|
+
}
|
|
94
|
+
// Keep complete exchanges: don't begin a truncated transcript with a detached answer.
|
|
95
|
+
while (start < request.history.length && request.history[start].role === "assistant") {
|
|
96
|
+
historySize -= bytes(request.history[start].text) + 64;
|
|
97
|
+
start += 1;
|
|
98
|
+
}
|
|
99
|
+
const mainBudget = Math.max(0, available - historySize);
|
|
100
|
+
const mainTruncated = bytes(mainText) > mainBudget;
|
|
101
|
+
let background = mainText;
|
|
102
|
+
if (mainTruncated) {
|
|
103
|
+
const remainder = Math.max(0, mainBudget - bytes(OMITTED));
|
|
104
|
+
const head = Math.floor(remainder / 5);
|
|
105
|
+
background = byteSlice(mainText, head) + OMITTED + byteSlice(mainText, remainder - head, true);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
maxTokens,
|
|
109
|
+
// 弹窗里的分项:这两块是旁聊自己的,字面长度就是实际发出去的字符数;
|
|
110
|
+
// 提问与历史回答由前端按消息分类统计(和主界面同一套)
|
|
111
|
+
characters: { systemPrompt: SYSTEM_PROMPT.length, background: (MAIN_PREFIX + background + MAIN_SUFFIX).length },
|
|
112
|
+
contextTruncated: mainTruncated || start > 0,
|
|
113
|
+
context: {
|
|
114
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
115
|
+
tools: [],
|
|
116
|
+
messages: [
|
|
117
|
+
...(background ? [{ role: "user" as const, content: MAIN_PREFIX + background + MAIN_SUFFIX, timestamp: 0 }] : []),
|
|
118
|
+
...request.history.slice(start).map((item) => historyMessage(item, model)),
|
|
119
|
+
{ role: "user", content: request.text, timestamp: Date.now() },
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { clampThinkingLevel, type AssistantMessage, type ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
2
|
+
import { getManaged } from "../agent-manager";
|
|
3
|
+
import { BoundaryError } from "../path-security";
|
|
4
|
+
import { getModelRuntime } from "../pi";
|
|
5
|
+
import { buildSideChatContext, mainConversationText } from "./context";
|
|
6
|
+
import type { SideChatProgress, SideChatRequest, SideChatUsage } from "./types";
|
|
7
|
+
|
|
8
|
+
const IDLE_MS = 10 * 60_000;
|
|
9
|
+
const RUN_MS = 15 * 60_000;
|
|
10
|
+
const MAX_RUNNING = 8;
|
|
11
|
+
|
|
12
|
+
interface SideChatJob {
|
|
13
|
+
progress: SideChatProgress;
|
|
14
|
+
controller: AbortController;
|
|
15
|
+
listeners: Set<() => void>;
|
|
16
|
+
lastSeen: number;
|
|
17
|
+
versions: Map<number, string>;
|
|
18
|
+
publishTimer?: ReturnType<typeof setTimeout>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const globalJobs = globalThis as typeof globalThis & {
|
|
22
|
+
__piWebSideChatJobs?: Map<string, SideChatJob>;
|
|
23
|
+
__piWebSideChatStops?: Map<string, number>;
|
|
24
|
+
};
|
|
25
|
+
const jobs = globalJobs.__piWebSideChatJobs ??= new Map();
|
|
26
|
+
const stopped = globalJobs.__piWebSideChatStops ??= new Map();
|
|
27
|
+
const keyOf = (sessionPath: string, requestId: string) => `${sessionPath}\0${requestId}`;
|
|
28
|
+
|
|
29
|
+
function reapJobs(): void {
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
for (const [key, job] of jobs) {
|
|
32
|
+
if (now - job.lastSeen <= IDLE_MS) continue;
|
|
33
|
+
job.controller.abort();
|
|
34
|
+
if (job.publishTimer) clearTimeout(job.publishTimer);
|
|
35
|
+
for (const notify of job.listeners) notify();
|
|
36
|
+
jobs.delete(key);
|
|
37
|
+
}
|
|
38
|
+
for (const [key, until] of stopped) if (until < now) stopped.delete(key);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function publish(job: SideChatJob, terminal = false): void {
|
|
42
|
+
if (job.publishTimer) {
|
|
43
|
+
if (!terminal) return;
|
|
44
|
+
clearTimeout(job.publishTimer);
|
|
45
|
+
}
|
|
46
|
+
const flush = () => {
|
|
47
|
+
job.publishTimer = undefined;
|
|
48
|
+
job.progress = { ...job.progress, revision: job.progress.revision + 1 };
|
|
49
|
+
job.versions.set(job.progress.revision, job.progress.text);
|
|
50
|
+
while (job.versions.size > 8) job.versions.delete(job.versions.keys().next().value!);
|
|
51
|
+
for (const notify of job.listeners) notify();
|
|
52
|
+
};
|
|
53
|
+
if (terminal) flush();
|
|
54
|
+
else job.publishTimer = setTimeout(flush, 80);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function settle(job: SideChatJob, update: Partial<SideChatProgress>): void {
|
|
58
|
+
if (job.progress.done) return;
|
|
59
|
+
job.progress = { ...job.progress, ...update, done: true };
|
|
60
|
+
publish(job, true);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 模型响应里的真实用量;拿不到就返回 undefined(不编数字) */
|
|
64
|
+
function usageOf(message: AssistantMessage): SideChatUsage | undefined {
|
|
65
|
+
const usage = message.usage;
|
|
66
|
+
if (!usage) return undefined;
|
|
67
|
+
return { input: usage.input ?? 0, output: usage.output ?? 0, cacheRead: usage.cacheRead ?? 0, cacheWrite: usage.cacheWrite ?? 0 };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function assistantText(message: AssistantMessage): string {
|
|
71
|
+
return message.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("\n\n");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Creates a separate model call. It never prompts, branches, aborts or saves the main AgentSession. */
|
|
75
|
+
export function startSideChat(sessionPath: string, request: SideChatRequest): SideChatProgress {
|
|
76
|
+
reapJobs();
|
|
77
|
+
const key = keyOf(sessionPath, request.requestId);
|
|
78
|
+
const previous = jobs.get(key);
|
|
79
|
+
if (previous) return { ...previous.progress };
|
|
80
|
+
if ([...jobs.values()].filter((job) => !job.progress.done).length >= MAX_RUNNING) throw new BoundaryError("too many side-chat answers are running");
|
|
81
|
+
const managed = getManaged(sessionPath);
|
|
82
|
+
const source = managed.sm.buildSessionContext();
|
|
83
|
+
const selected = managed.session?.model;
|
|
84
|
+
const provider = request.model?.provider ?? selected?.provider ?? source.model?.provider;
|
|
85
|
+
const modelId = request.model?.id ?? selected?.id ?? source.model?.modelId;
|
|
86
|
+
if (!provider || !modelId) throw new BoundaryError("select a model for the side chat first");
|
|
87
|
+
const thinking = managed.session?.thinkingLevel ?? source.thinkingLevel;
|
|
88
|
+
const messages: unknown[] = [...source.messages];
|
|
89
|
+
// A question may refer to the answer currently streaming in the main pane.
|
|
90
|
+
if (managed.inflight && !source.messages.includes(managed.inflight as typeof source.messages[number])) messages.push(managed.inflight);
|
|
91
|
+
const background = mainConversationText(messages);
|
|
92
|
+
const job: SideChatJob = {
|
|
93
|
+
progress: { requestId: request.requestId, revision: 0, text: "", done: false, aborted: false },
|
|
94
|
+
controller: new AbortController(), listeners: new Set(), lastSeen: Date.now(), versions: new Map([[0, ""]]),
|
|
95
|
+
};
|
|
96
|
+
jobs.set(key, job);
|
|
97
|
+
if (stopped.has(key)) {
|
|
98
|
+
settle(job, { aborted: true });
|
|
99
|
+
return { ...job.progress };
|
|
100
|
+
}
|
|
101
|
+
void generate();
|
|
102
|
+
return { ...job.progress };
|
|
103
|
+
|
|
104
|
+
async function generate(): Promise<void> {
|
|
105
|
+
const timeout = setTimeout(() => {
|
|
106
|
+
job.controller.abort();
|
|
107
|
+
settle(job, { error: "side-chat request timed out" });
|
|
108
|
+
}, RUN_MS);
|
|
109
|
+
timeout.unref();
|
|
110
|
+
try {
|
|
111
|
+
const runtime = await getModelRuntime();
|
|
112
|
+
if (job.controller.signal.aborted) return;
|
|
113
|
+
const model = runtime.getModel(provider!, modelId!);
|
|
114
|
+
if (!model) throw new BoundaryError("the selected side-chat model is unavailable");
|
|
115
|
+
const built = buildSideChatContext(background, request, model);
|
|
116
|
+
const level = clampThinkingLevel(model, (thinking || "off") as ModelThinkingLevel);
|
|
117
|
+
job.progress = { ...job.progress, model: model.name, contextWindow: model.contextWindow, characters: built.characters, contextTruncated: built.contextTruncated };
|
|
118
|
+
publish(job);
|
|
119
|
+
const stream = runtime.streamSimple(model, built.context, {
|
|
120
|
+
signal: job.controller.signal,
|
|
121
|
+
reasoning: level === "off" ? undefined : level,
|
|
122
|
+
maxTokens: built.maxTokens,
|
|
123
|
+
sessionId: `piweb-sidechat-${request.threadId}`,
|
|
124
|
+
transport: "sse",
|
|
125
|
+
});
|
|
126
|
+
const blocks = new Map<number, string>();
|
|
127
|
+
for await (const event of stream) {
|
|
128
|
+
if (job.progress.done) break;
|
|
129
|
+
if (event.type === "text_delta") {
|
|
130
|
+
blocks.set(event.contentIndex, (blocks.get(event.contentIndex) ?? "") + event.delta);
|
|
131
|
+
job.progress = { ...job.progress, text: [...blocks.entries()].sort(([a], [b]) => a - b).map(([, text]) => text).join("\n\n") };
|
|
132
|
+
publish(job);
|
|
133
|
+
} else if (event.type === "done") {
|
|
134
|
+
const usage = usageOf(event.message);
|
|
135
|
+
settle(job, { text: assistantText(event.message), lengthLimited: event.reason === "length", ...(usage ? { usage } : {}) });
|
|
136
|
+
} else if (event.type === "error") {
|
|
137
|
+
// 出错时的响应里也可能有 usage(比如跑一半被截断),有就记下来
|
|
138
|
+
const usage = usageOf(event.error);
|
|
139
|
+
settle(job, { text: assistantText(event.error), aborted: event.reason === "aborted", error: event.reason === "aborted" ? undefined : event.error.errorMessage || "model request failed", ...(usage ? { usage } : {}) });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!job.progress.done) settle(job, { aborted: job.controller.signal.aborted, error: job.controller.signal.aborted ? undefined : "model stream ended before completion" });
|
|
143
|
+
} catch (error) {
|
|
144
|
+
settle(job, { aborted: job.controller.signal.aborted, error: job.controller.signal.aborted ? undefined : error instanceof Error ? error.message : "side-chat request failed" });
|
|
145
|
+
} finally {
|
|
146
|
+
clearTimeout(timeout);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function getJob(sessionPath: string, requestId: string): SideChatJob {
|
|
152
|
+
reapJobs();
|
|
153
|
+
const job = jobs.get(keyOf(sessionPath, requestId));
|
|
154
|
+
if (!job) throw new BoundaryError("side-chat answer expired; send the question again");
|
|
155
|
+
job.lastSeen = Date.now();
|
|
156
|
+
return job;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Finite JSON responses also work through Cloudflare's buffering quick tunnels. */
|
|
160
|
+
export async function pollSideChat(sessionPath: string, requestId: string, after: number, signal: AbortSignal): Promise<SideChatProgress> {
|
|
161
|
+
const job = getJob(sessionPath, requestId);
|
|
162
|
+
if (!job.progress.done && job.progress.revision <= after && !signal.aborted) {
|
|
163
|
+
await new Promise<void>((resolve) => {
|
|
164
|
+
const finish = () => {
|
|
165
|
+
clearTimeout(timer);
|
|
166
|
+
job.listeners.delete(finish);
|
|
167
|
+
signal.removeEventListener("abort", finish);
|
|
168
|
+
resolve();
|
|
169
|
+
};
|
|
170
|
+
const timer = setTimeout(finish, 20_000);
|
|
171
|
+
job.listeners.add(finish);
|
|
172
|
+
signal.addEventListener("abort", finish, { once: true });
|
|
173
|
+
if (signal.aborted || job.progress.done || job.progress.revision > after) finish();
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
const previous = job.versions.get(after);
|
|
177
|
+
if (previous !== undefined && job.progress.text.startsWith(previous)) {
|
|
178
|
+
return { ...job.progress, text: job.progress.text.slice(previous.length), textOffset: previous.length };
|
|
179
|
+
}
|
|
180
|
+
return { ...job.progress, textOffset: 0 };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function stopSideChat(sessionPath: string, requestId: string): void {
|
|
184
|
+
const key = keyOf(sessionPath, requestId);
|
|
185
|
+
// Remember an early stop so cancelling while POST is in transit cannot start a late model call.
|
|
186
|
+
stopped.set(key, Date.now() + 60_000);
|
|
187
|
+
const job = jobs.get(key);
|
|
188
|
+
if (!job) return;
|
|
189
|
+
job.controller.abort();
|
|
190
|
+
settle(job, { aborted: true });
|
|
191
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Shared side-chat data. Main session messages never contain these records. */
|
|
2
|
+
export interface SideChatHistoryMessage {
|
|
3
|
+
role: "user" | "assistant";
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface SideChatModel {
|
|
8
|
+
provider: string;
|
|
9
|
+
id: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isSideChatModel(value: unknown): value is SideChatModel {
|
|
13
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
14
|
+
const model = value as Record<string, unknown>;
|
|
15
|
+
return [model.provider, model.id].every((part) => typeof part === "string" && part.trim().length > 0 && part.length <= 512);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SideChatRequest {
|
|
19
|
+
requestId: string;
|
|
20
|
+
threadId: string;
|
|
21
|
+
text: string;
|
|
22
|
+
history: SideChatHistoryMessage[];
|
|
23
|
+
/** Omitted means follow the main conversation's current model. */
|
|
24
|
+
model?: SideChatModel;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 一次旁聊回答的真实用量,直接来自模型响应(不是估算)。
|
|
29
|
+
* 它同时就是「这次问出去多少上下文」——input + 缓存读写就是发出去的 prompt 大小。
|
|
30
|
+
*/
|
|
31
|
+
export interface SideChatUsage {
|
|
32
|
+
input: number;
|
|
33
|
+
output: number;
|
|
34
|
+
cacheRead: number;
|
|
35
|
+
cacheWrite: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SideChatProgress {
|
|
39
|
+
requestId: string;
|
|
40
|
+
revision: number;
|
|
41
|
+
text: string;
|
|
42
|
+
/** Poll responses may replace the text from this UTF-16 offset, avoiding repeated full answers. */
|
|
43
|
+
textOffset?: number;
|
|
44
|
+
done: boolean;
|
|
45
|
+
aborted: boolean;
|
|
46
|
+
error?: string;
|
|
47
|
+
model?: string;
|
|
48
|
+
/** 该回答用的模型上下文窗口;拿不到就不写,界面宁可什么都不显示也不显示假数字 */
|
|
49
|
+
contextWindow?: number;
|
|
50
|
+
usage?: SideChatUsage;
|
|
51
|
+
/** 拼这次上下文时的真实字符数(分项用;系统提示词 + 引用的主会话背景) */
|
|
52
|
+
characters?: { systemPrompt: number; background: number };
|
|
53
|
+
contextTruncated?: boolean;
|
|
54
|
+
lengthLimited?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const SIDE_CHAT_BODY_LIMIT = 2 * 1024 * 1024;
|
|
58
|
+
export const SIDE_CHAT_TEXT_LIMIT = 100_000;
|
|
59
|
+
export const SIDE_CHAT_HISTORY_LIMIT = 1000;
|
|
60
|
+
export const isSideChatId = (value: unknown): value is string =>
|
|
61
|
+
typeof value === "string" && /^[a-zA-Z0-9_-]{8,80}$/.test(value);
|
|
@@ -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
|
@@ -145,8 +145,10 @@ export type WebEvent =
|
|
|
145
145
|
}
|
|
146
146
|
| { type: "extension_ui_resolved"; id: string; ts: number }
|
|
147
147
|
| { type: "error"; message: string; ts: number }
|
|
148
|
-
/**
|
|
148
|
+
/** 自动重试:流程内通知,界面聚合成一条「重试 n/max」,不作为错误条目累积 */
|
|
149
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 }
|
|
150
152
|
/** 项目生长:提交了一个 commit(pi 的一轮 / 用户两轮之间的修改 / 工作区基线) */
|
|
151
153
|
| { type: "growth"; round: GrowthRound; ts: number }
|
|
152
154
|
| { type: "growth_error"; message: string | null; ts: number }
|
|
@@ -166,6 +168,11 @@ export interface WebStats {
|
|
|
166
168
|
ttftSteps: number;
|
|
167
169
|
decodeMs: number;
|
|
168
170
|
decodeTokens: number;
|
|
171
|
+
/** 有持久化耗时的消息 / 工具数量;旧历史缺数据时显示覆盖范围。 */
|
|
172
|
+
llmTimedSteps?: number;
|
|
173
|
+
toolTimedCalls?: number;
|
|
174
|
+
/** 生成速度实际测到的回复数,不代表整个历史。 */
|
|
175
|
+
decodeSteps?: number;
|
|
169
176
|
}
|
|
170
177
|
|
|
171
178
|
// ---------- 项目生长(每轮一个 git commit) ----------
|
|
@@ -236,6 +243,8 @@ export interface ContextResource {
|
|
|
236
243
|
* 这里只放服务端才能拿到的部分。
|
|
237
244
|
*/
|
|
238
245
|
export interface ContextBreakdown {
|
|
246
|
+
/** 当前投影中实际文本的字符数;不将估算 token 按比例分摊。 */
|
|
247
|
+
characters?: { systemPrompt: number; systemTools: number; customTools: number; memory: number; skills: number; compacted: number;/** 旁聊专用:这一问里引用的主会话背景字符数 */ background?: number };
|
|
239
248
|
/** SDK 内建 system prompt 正文(不含 memory/skills/tools 段)的估算 token */
|
|
240
249
|
systemPrompt: number;
|
|
241
250
|
/** 内建工具(SDK 自带 bash/read/edit 等)定义的估算 token */
|
|
@@ -338,6 +347,8 @@ export interface AgentCommand {
|
|
|
338
347
|
instructions?: string;
|
|
339
348
|
provider?: string;
|
|
340
349
|
modelId?: string;
|
|
350
|
+
/** setModel:新会话页选的模型,思考档位按新会话的规则定(不沿用会话原来的档位) */
|
|
351
|
+
fresh?: boolean;
|
|
341
352
|
level?: string;
|
|
342
353
|
preset?: ToolPreset;
|
|
343
354
|
entryId?: string;
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import fs from "node:fs/promises";
|
|
7
7
|
import path from "node:path";
|
|
8
|
-
import { installPiUpdate,
|
|
8
|
+
import { installPiUpdate, PI_PACKAGE, prepareRelease } from "../../scripts/release.mjs";
|
|
9
|
+
import { installPiRuntime } from "../../scripts/runtime/pi-packages.mjs";
|
|
9
10
|
import { assertInstallation, readProductionBuild } from "../../scripts/build-output.mjs";
|
|
10
11
|
import { runCommand } from "../../scripts/process-runner.mjs";
|
|
11
12
|
import { isNewer } from "./semver";
|
|
@@ -155,15 +156,12 @@ export function createUpdateService({ root = APP_ROOT, run = runCommand, version
|
|
|
155
156
|
|
|
156
157
|
/**
|
|
157
158
|
* 只换 npm 成品包里的 pi 依赖:成品包没有 devDependencies,跑不了 typecheck / 构建,也不需要——
|
|
158
|
-
* pi
|
|
159
|
+
* pi 引擎和 pi-ai 都在 serverExternalPackages 里;配套依赖同步锁到同一版本,重启即用上新版。
|
|
159
160
|
* Windows 也能做:运行中的服务没加载 pi 的原生模块,pi 目录也不是服务的工作目录,不会 EBUSY。
|
|
160
161
|
* 图标不用同步:成品包自带 public/provider-icons,清单也和 pi 版本无关。
|
|
161
162
|
*/
|
|
162
163
|
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
|
-
});
|
|
164
|
+
await installPiRuntime({ root: installation, env: commandEnv, run }, { omitDev: true, assertIdle });
|
|
167
165
|
await assertIdle();
|
|
168
166
|
assertInstallation(installation);
|
|
169
167
|
const version = await readPackageVersion(path.join(installation, "node_modules", PI_PACKAGE, "package.json"));
|