@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
package/src/lib/agent-manager.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
|
|
6
6
|
import { randomUUID } from "node:crypto";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import type { AgentSession, AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import { generateUnifiedPatch } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { formatSkillsForPrompt, generateUnifiedPatch } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import {
|
|
11
11
|
getResourceLoader,
|
|
12
12
|
createSessionResourceLoader,
|
|
@@ -25,17 +25,18 @@ import {
|
|
|
25
25
|
SessionManager,
|
|
26
26
|
createAgentSession,
|
|
27
27
|
getModelRuntime,
|
|
28
|
+
refreshModelRuntime,
|
|
28
29
|
openSessionManager,
|
|
29
30
|
} from "./pi";
|
|
30
31
|
import { createExtensionUiBridge, type ExtensionUiBridge } from "./extension-ui";
|
|
31
32
|
import { createGrowthTracker, GROWTH_TRACKER_VERSION, type GrowthTracker } from "./growth-tracker";
|
|
32
33
|
import { userTurnsFromEntries } from "./growth-turns";
|
|
34
|
+
import { projectedContextUsage } from "./session/metrics";
|
|
33
35
|
import { TrajLedger, buildTrajectoryFromEntries, toTrajTokens } from "./trajectory";
|
|
34
36
|
import { startArchivedSessionRetention } from "./session-retention";
|
|
35
37
|
import { sanitizeToolOutput } from "./text-sanitize";
|
|
36
38
|
import { BoundaryError } from "./path-security";
|
|
37
|
-
import {
|
|
38
|
-
import { getPiSettings } from "./pi-settings";
|
|
39
|
+
import { newSessionThinkingLevel } from "./thinking-level";
|
|
39
40
|
import { getGrowthInitConsent, setGrowthInitConsent } from "./workspace-store";
|
|
40
41
|
import { clampThinkingLevel, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
41
42
|
import type {
|
|
@@ -63,6 +64,12 @@ interface Managed {
|
|
|
63
64
|
session: AgentSession | null;
|
|
64
65
|
creating: Promise<AgentSession> | null;
|
|
65
66
|
subscribers: Set<(seq: number, json: string) => void>;
|
|
67
|
+
/** 长轮询的浏览器在两次请求之间没有订阅,到这个时间点之前仍算有人在看(见 holdViewer) */
|
|
68
|
+
viewerGraceUntil?: number;
|
|
69
|
+
/** 本轮已排队推送上下文分项(publishContextBreakdown 合并同一轮的多次触发) */
|
|
70
|
+
breakdownQueued?: boolean;
|
|
71
|
+
/** 首次运行后 pi 才会把系统分段持久化,届时用它把分类从差分换成投影口径,只换一次 */
|
|
72
|
+
breakdownProjected?: boolean;
|
|
66
73
|
buffer: { seq: number; json: string }[];
|
|
67
74
|
seq: number;
|
|
68
75
|
/**
|
|
@@ -345,36 +352,58 @@ function reapplyDesiredLevel(m: Managed, session: AgentSession): void {
|
|
|
345
352
|
if (target !== session.thinkingLevel) session.setThinkingLevel(target as never);
|
|
346
353
|
}
|
|
347
354
|
|
|
355
|
+
/**
|
|
356
|
+
* 新会话页选定模型后的思考档位:与界面显示的生效值同一规则(每模型覆盖 → 设置默认 → medium,再按模型能力钳制)。
|
|
357
|
+
* SDK 切换模型时会沿用会话原来的档位,新会话不能沿用:那个档位是 SDK 按它自己的初始模型定的。
|
|
358
|
+
*/
|
|
359
|
+
function applyNewSessionThinking(m: Managed, session: AgentSession): void {
|
|
360
|
+
const model = session.model;
|
|
361
|
+
if (!model) return;
|
|
362
|
+
const settings = getSettingsManager(m.cwd);
|
|
363
|
+
const level = newSessionThinkingLevel({
|
|
364
|
+
supported: session.getAvailableThinkingLevels() as unknown as string[],
|
|
365
|
+
modelOverride: settings.getModelThinkingLevel(model.provider, model.id),
|
|
366
|
+
defaultLevel: settings.getDefaultThinkingLevel(),
|
|
367
|
+
});
|
|
368
|
+
if (level !== session.thinkingLevel) session.setThinkingLevel(level as never);
|
|
369
|
+
}
|
|
370
|
+
|
|
348
371
|
function publishTraj(m: Managed): void {
|
|
349
372
|
for (const entry of m.ledger.drainDirty()) {
|
|
350
373
|
publish(m, { type: "traj", entry, ts: Date.now() });
|
|
351
374
|
}
|
|
352
375
|
}
|
|
353
376
|
|
|
354
|
-
function timingStats(m: Managed): Pick<WebStats, "llmMs" | "toolMs" | "ttftMs" | "ttftSteps" | "decodeMs" | "decodeTokens"> {
|
|
377
|
+
function timingStats(m: Managed): Pick<WebStats, "llmMs" | "toolMs" | "ttftMs" | "ttftSteps" | "decodeMs" | "decodeTokens" | "llmTimedSteps" | "toolTimedCalls" | "decodeSteps"> {
|
|
355
378
|
let llmMs = 0;
|
|
356
379
|
let toolMs = 0;
|
|
357
380
|
let ttftMs = 0;
|
|
358
381
|
let ttftSteps = 0;
|
|
359
382
|
let decodeMs = 0;
|
|
360
383
|
let decodeTokens = 0;
|
|
384
|
+
let llmTimedSteps = 0;
|
|
385
|
+
let toolTimedCalls = 0;
|
|
386
|
+
let decodeSteps = 0;
|
|
361
387
|
for (const entry of m.ledger.entries) {
|
|
362
388
|
if (entry.kind === "tool") {
|
|
389
|
+
if (entry.timing?.durationMs !== undefined) toolTimedCalls++;
|
|
363
390
|
toolMs += Math.max(0, entry.timing?.durationMs ?? 0);
|
|
364
391
|
continue;
|
|
365
392
|
}
|
|
366
393
|
if (entry.kind !== "message") continue;
|
|
394
|
+
if (entry.timing?.durationMs !== undefined) llmTimedSteps++;
|
|
367
395
|
llmMs += Math.max(0, entry.timing?.durationMs ?? 0);
|
|
368
396
|
if (entry.timing?.ttftMs !== undefined) {
|
|
369
397
|
ttftMs += Math.max(0, entry.timing.ttftMs);
|
|
370
398
|
ttftSteps += 1;
|
|
371
399
|
}
|
|
372
|
-
if (entry.timing?.decodeMs
|
|
373
|
-
|
|
400
|
+
if ((entry.timing?.decodeMs ?? 0) > 0 && entry.tokens?.output !== undefined) {
|
|
401
|
+
decodeSteps++;
|
|
402
|
+
decodeMs += Math.max(0, entry.timing?.decodeMs ?? 0);
|
|
374
403
|
decodeTokens += Math.max(0, entry.tokens.output);
|
|
375
404
|
}
|
|
376
405
|
}
|
|
377
|
-
return { llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens };
|
|
406
|
+
return { llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens, llmTimedSteps, toolTimedCalls, decodeSteps };
|
|
378
407
|
}
|
|
379
408
|
|
|
380
409
|
function sessionStats(m: Managed): WebStats | null {
|
|
@@ -411,7 +440,7 @@ function coldSessionStats(m: Managed): WebStats {
|
|
|
411
440
|
cost += Number(usage.cost?.total ?? 0);
|
|
412
441
|
};
|
|
413
442
|
for (const entry of m.sm.getEntries() as any[]) {
|
|
414
|
-
if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) addUsage(entry.usage);
|
|
443
|
+
if ((entry.type === "usage" || entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) addUsage(entry.usage);
|
|
415
444
|
if (entry.type !== "message" || !entry.message) continue;
|
|
416
445
|
totalMessages += 1;
|
|
417
446
|
const message = entry.message;
|
|
@@ -493,7 +522,14 @@ function translate(m: Managed, evt: AgentSessionEvent): void {
|
|
|
493
522
|
// the browser receives the durable entry ID without reordering later tool events.
|
|
494
523
|
deferPublish(m, () => {
|
|
495
524
|
const msg = projectMessage(m, raw);
|
|
496
|
-
if (msg.role === "assistant")
|
|
525
|
+
if (msg.role === "assistant") {
|
|
526
|
+
msg.endedAt = now;
|
|
527
|
+
// 首次回复结束后 pi 已把系统分段写进会话文件,此时分类能读到投影(比 systemPrompt 差分精确)
|
|
528
|
+
if (!m.breakdownProjected) {
|
|
529
|
+
m.breakdownProjected = true;
|
|
530
|
+
publishContextBreakdown(m);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
497
533
|
const usage = usageEvent(m);
|
|
498
534
|
return usage
|
|
499
535
|
? [{ type: "message", message: msg, phase: "end", ts: now }, usage]
|
|
@@ -579,6 +615,7 @@ function translate(m: Managed, evt: AgentSessionEvent): void {
|
|
|
579
615
|
ts: now,
|
|
580
616
|
});
|
|
581
617
|
publishUsage(m);
|
|
618
|
+
publishContextBreakdown(m);
|
|
582
619
|
if ((evt as any).reason === "manual") {
|
|
583
620
|
publish(m, { type: "status", isStreaming: isManagedBusy(m), state: isManagedBusy(m) ? "running" : "idle", ts: now });
|
|
584
621
|
if (m.resourceReloadPending) queueMicrotask(() => void reloadManagedResources(m));
|
|
@@ -592,8 +629,8 @@ function translate(m: Managed, evt: AgentSessionEvent): void {
|
|
|
592
629
|
else publish(m, { type: "model", thinkingLevel: (evt as any).level, ts: now });
|
|
593
630
|
break;
|
|
594
631
|
case "auto_retry_start":
|
|
595
|
-
//
|
|
596
|
-
// 不再走 error
|
|
632
|
+
// 重试是流程内通知:发一条结构化 retry 事件(界面聚合成「重试 n/max」一行),
|
|
633
|
+
// 不再走 error 通道,免得每次尝试都多出一条报错。
|
|
597
634
|
publish(m, {
|
|
598
635
|
type: "retry",
|
|
599
636
|
attempt: Number((evt as any).attempt) || 0,
|
|
@@ -602,6 +639,13 @@ function translate(m: Managed, evt: AgentSessionEvent): void {
|
|
|
602
639
|
ts: now,
|
|
603
640
|
});
|
|
604
641
|
break;
|
|
642
|
+
case "auto_retry_end":
|
|
643
|
+
// 重试收尾:SDK 成功/最终失败/被中止都会发这条,而重试成功后的这轮对话
|
|
644
|
+
// agent_settled 之前界面上还留着「重试 1/5 terminated」那一行(用户看到的
|
|
645
|
+
// 就是「已经恢复了还显示错误」)。这里显式发一条清除事件,重试区间一结束
|
|
646
|
+
// 就收掉这行;真失败时紧随其后的 assistant error 消息会照常出错误块。
|
|
647
|
+
publish(m, { type: "retry_end", attempt: Number((evt as any).attempt) || 0, ts: now });
|
|
648
|
+
break;
|
|
605
649
|
default:
|
|
606
650
|
break;
|
|
607
651
|
}
|
|
@@ -733,87 +777,99 @@ function loadContextResources(cwd: string, loader: AgentSession["resourceLoader"
|
|
|
733
777
|
];
|
|
734
778
|
}
|
|
735
779
|
|
|
780
|
+
const SKILLS_SECTION_START = "The following skills provide specialized instructions";
|
|
781
|
+
const SKILLS_SECTION_END = "</available_skills>";
|
|
782
|
+
|
|
736
783
|
/**
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
*
|
|
784
|
+
* 当前上下文的字符分类,优先读 SDK 投影中的系统分段、工具 schema 与摘要。
|
|
785
|
+
* 旧客户端仍可读取字符/4 的估算字段;新客户端仅将字符数作为分类数据。
|
|
786
|
+
* 没有持久化系统消息且尚未启动的会话,资源来自发现缓存并标记 cold。
|
|
740
787
|
*/
|
|
741
|
-
async function buildContextBreakdown(
|
|
742
|
-
|
|
743
|
-
session: AgentSession | null,
|
|
744
|
-
resources: ContextResource[],
|
|
745
|
-
): Promise<ContextBreakdown> {
|
|
788
|
+
async function buildContextBreakdown(m: Managed, resources: ContextResource[]): Promise<ContextBreakdown> {
|
|
789
|
+
const session = m.session;
|
|
746
790
|
const breakdown: ContextBreakdown = {
|
|
747
791
|
systemPrompt: 0, systemTools: 0, customTools: 0, memory: 0, skills: 0, compacted: 0, autoCompactBuffer: 0,
|
|
792
|
+
...(session ? {} : { cold: true }),
|
|
748
793
|
};
|
|
749
|
-
|
|
794
|
+
// Memory:AGENTS.md 等注入文件(contextResources 的 project 部分)
|
|
795
|
+
const memoryText = resources.filter((r) => r.source === "project").map((r) => r.content).join("\n\n");
|
|
796
|
+
// 字符分项只读取当前投影中的内容。历史压缩摘要、未激活工具不混入当前统计。
|
|
797
|
+
const characters = { systemPrompt: 0, systemTools: 0, customTools: 0, memory: 0, skills: 0, compacted: 0 };
|
|
750
798
|
try {
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
799
|
+
const messages = m.sm.buildSessionProjection().messages as any[];
|
|
800
|
+
const system = messages.findLast((message) => message.role === "system");
|
|
801
|
+
if (system) {
|
|
802
|
+
const contentChars = typeof system.content === "string" ? system.content.length : textOf(system.content).length;
|
|
803
|
+
characters.memory = system.sections?.project_context?.length ?? 0;
|
|
804
|
+
characters.skills = system.sections?.skills?.length ?? 0;
|
|
805
|
+
characters.systemPrompt = contentChars + Object.entries(system.sections ?? {})
|
|
806
|
+
.filter(([key]) => key !== "project_context" && key !== "skills")
|
|
807
|
+
.reduce((sum, [, value]) => sum + (typeof value === "string" ? value.length : 0), 0);
|
|
808
|
+
const builtinNames = new Set(session
|
|
809
|
+
? session.getAllTools().filter((tool: any) => tool.sourceInfo?.source === "builtin").map((tool) => tool.name)
|
|
810
|
+
: ["read", "bash", "edit", "write", "grep", "find", "ls"]);
|
|
811
|
+
for (const tool of system.toolsAdded ?? []) {
|
|
812
|
+
characters[builtinNames.has(tool.name) ? "systemTools" : "customTools"] += JSON.stringify(tool).length;
|
|
764
813
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
const
|
|
774
|
-
for (const tool of
|
|
775
|
-
const size =
|
|
776
|
-
|
|
777
|
-
description: tool.description,
|
|
778
|
-
parameters: tool.parameters,
|
|
779
|
-
promptGuidelines: tool.promptGuidelines,
|
|
780
|
-
}));
|
|
781
|
-
if ((tool as { sourceInfo?: { source?: string } }).sourceInfo?.source === "builtin") breakdown.systemTools += size;
|
|
782
|
-
else breakdown.customTools += size;
|
|
814
|
+
delete breakdown.cold;
|
|
815
|
+
} else if (session) {
|
|
816
|
+
const prompt = session.systemPrompt ?? "";
|
|
817
|
+
characters.memory = memoryText.length;
|
|
818
|
+
const start = prompt.indexOf(SKILLS_SECTION_START);
|
|
819
|
+
const end = prompt.indexOf(SKILLS_SECTION_END);
|
|
820
|
+
characters.skills = start >= 0 && end > start ? prompt.slice(start, end + SKILLS_SECTION_END.length).length : 0;
|
|
821
|
+
characters.systemPrompt = Math.max(0, prompt.length - characters.memory - characters.skills);
|
|
822
|
+
const active = new Set(session.getActiveToolNames());
|
|
823
|
+
for (const tool of session.getAllTools().filter((tool) => active.has(tool.name))) {
|
|
824
|
+
const size = JSON.stringify({ name: tool.name, description: tool.description, parameters: tool.parameters }).length;
|
|
825
|
+
characters[(tool as any).sourceInfo?.source === "builtin" ? "systemTools" : "customTools"] += size;
|
|
783
826
|
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
const entries = m_smEntries(session);
|
|
790
|
-
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
791
|
-
const entry = entries[i] as { type?: string; summary?: string };
|
|
792
|
-
if (entry?.type === "compaction" && typeof entry.summary === "string") {
|
|
793
|
-
breakdown.compacted = estimateTokensOf(entry.summary);
|
|
794
|
-
break;
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
} catch {
|
|
798
|
-
/* entries 不可用则跳过 */
|
|
827
|
+
} else {
|
|
828
|
+
// 初次启动前只能读到待注入的资源,界面保留 cold 提示。
|
|
829
|
+
characters.memory = memoryText.length;
|
|
830
|
+
const loader = peekResourceLoader(m.cwd) ?? getResourceLoader(m.cwd);
|
|
831
|
+
characters.skills = formatSkillsForPrompt(loader.getSkills().skills).trim().length;
|
|
799
832
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
const settings = await getPiSettings();
|
|
803
|
-
if (settings.compaction.enabled) breakdown.autoCompactBuffer = settings.compaction.reserveTokens;
|
|
804
|
-
} catch {
|
|
805
|
-
/* 设置读取失败则保持 0 */
|
|
833
|
+
for (const message of messages) {
|
|
834
|
+
if (message.role === "compactionSummary" || message.role === "branchSummary") characters.compacted += message.summary?.length ?? 0;
|
|
806
835
|
}
|
|
836
|
+
breakdown.characters = characters;
|
|
837
|
+
// 保留旧客户端的估算字段,当前客户端直接展示实际字符数。
|
|
838
|
+
for (const key of Object.keys(characters) as Array<keyof typeof characters>) breakdown[key] = Math.ceil(characters[key] / 4);
|
|
807
839
|
} catch {
|
|
808
|
-
/*
|
|
840
|
+
/* 投影不可用时不给字符分项,不能从旧 token 估算反推字符数。 */
|
|
841
|
+
}
|
|
842
|
+
// auto-compact 预留读本工作区、当前模型实际使用的设置。
|
|
843
|
+
try {
|
|
844
|
+
const compaction = getSettingsManager(m.cwd).getCompactionSettings(session?.model);
|
|
845
|
+
if (compaction.enabled) breakdown.autoCompactBuffer = compaction.reserveTokens;
|
|
846
|
+
} catch {
|
|
847
|
+
/* 设置读取失败则保持 0 */
|
|
809
848
|
}
|
|
810
849
|
return breakdown;
|
|
811
850
|
}
|
|
812
851
|
|
|
813
|
-
/**
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
852
|
+
/**
|
|
853
|
+
* 分项变了就推一次(会话启动、工具 / 资源重载、压缩之后)。分项以前只随快照下发:
|
|
854
|
+
* 打开冷会话时是 0,之后会话启动了弹窗也一直停在那份 0 上,直到刷新页面。
|
|
855
|
+
* 同一轮里多次触发(启动时 publishTools + publishResources)合并成一次。
|
|
856
|
+
*/
|
|
857
|
+
function publishContextBreakdown(m: Managed): void {
|
|
858
|
+
if (m.breakdownQueued || m.disposed) return;
|
|
859
|
+
m.breakdownQueued = true;
|
|
860
|
+
queueMicrotask(() => {
|
|
861
|
+
m.breakdownQueued = false;
|
|
862
|
+
if (m.disposed) return;
|
|
863
|
+
let resources: ContextResource[] = [];
|
|
864
|
+
try {
|
|
865
|
+
resources = loadContextResources(m.cwd, m.session?.resourceLoader);
|
|
866
|
+
} catch {
|
|
867
|
+
/* 读不到注入文件就按空算 Memory */
|
|
868
|
+
}
|
|
869
|
+
void buildContextBreakdown(m, resources).then((breakdown) => {
|
|
870
|
+
if (!m.disposed) publish(m, { type: "context_breakdown", breakdown, ts: Date.now() });
|
|
871
|
+
}, () => undefined);
|
|
872
|
+
});
|
|
817
873
|
}
|
|
818
874
|
|
|
819
875
|
/** resources 已经算过(快照)就传进来,免得资源发现结果再读一遍 */
|
|
@@ -877,7 +933,7 @@ export async function ensureSession(m: Managed): Promise<AgentSession> {
|
|
|
877
933
|
installSessionPolicy(m, session);
|
|
878
934
|
applyToolPolicy(m, session);
|
|
879
935
|
session.subscribe((evt) => { if (!m.disposed) translate(m, evt); });
|
|
880
|
-
const ui = createExtensionUiBridge({ publish: (evt) => publish(m, evt), hasViewers: () => !m.disposed && m
|
|
936
|
+
const ui = createExtensionUiBridge({ publish: (evt) => publish(m, evt), hasViewers: () => !m.disposed && hasViewers(m) });
|
|
881
937
|
m.ui = ui;
|
|
882
938
|
await session.bindExtensions({
|
|
883
939
|
mode: "rpc",
|
|
@@ -989,6 +1045,7 @@ function shutdownSession(session: AgentSession): Promise<void> {
|
|
|
989
1045
|
|
|
990
1046
|
function publishTools(m: Managed, session: AgentSession): void {
|
|
991
1047
|
publish(m, { type: "tools", active: listActiveTools(session), all: listAllToolNames(session), toolPreset: m.toolPreset, customActiveTools: m.customActiveTools ?? null, ts: Date.now() });
|
|
1048
|
+
publishContextBreakdown(m);
|
|
992
1049
|
}
|
|
993
1050
|
|
|
994
1051
|
/** SDK reload 自动启用扩展工具,注册新工具也会刷新全集;统一在激活边界收敛到服务端选择。 */
|
|
@@ -1086,13 +1143,13 @@ export async function disposeSessionPath(sessionPath: string): Promise<void> {
|
|
|
1086
1143
|
export function reap(): void {
|
|
1087
1144
|
const now = Date.now();
|
|
1088
1145
|
for (const m of [...sessions.values()]) {
|
|
1089
|
-
if (m
|
|
1146
|
+
if (hasViewers(m) || now - m.lastActive <= IDLE_MS) continue;
|
|
1090
1147
|
if (!m.creating && !isManagedBusy(m)) disposeSession(m);
|
|
1091
1148
|
}
|
|
1092
1149
|
const active = [...sessions.values()].filter((m) => m.session);
|
|
1093
1150
|
if (active.length > MAX_ACTIVE) {
|
|
1094
1151
|
const evictable = active
|
|
1095
|
-
.filter((m) => !m.creating && !isManagedBusy(m) && m
|
|
1152
|
+
.filter((m) => !m.creating && !isManagedBusy(m) && !hasViewers(m))
|
|
1096
1153
|
.sort((a, b) => a.lastActive - b.lastActive);
|
|
1097
1154
|
for (const m of evictable.slice(0, active.length - MAX_ACTIVE)) disposeSession(m);
|
|
1098
1155
|
}
|
|
@@ -1166,6 +1223,7 @@ function collectResources(m: Managed): ResourceBundle {
|
|
|
1166
1223
|
|
|
1167
1224
|
export function publishResources(m: Managed): void {
|
|
1168
1225
|
publish(m, { type: "resources", ...collectResources(m), ts: Date.now() });
|
|
1226
|
+
publishContextBreakdown(m);
|
|
1169
1227
|
}
|
|
1170
1228
|
|
|
1171
1229
|
/**
|
|
@@ -1177,11 +1235,7 @@ async function reloadManagedResources(m: Managed): Promise<boolean> {
|
|
|
1177
1235
|
m.resourceReloadPending = false;
|
|
1178
1236
|
try {
|
|
1179
1237
|
await m.session.reload();
|
|
1180
|
-
|
|
1181
|
-
// 会话手里还是旧对象。按 provider/id 从(已就地刷新的)共享 runtime 取一次新的。
|
|
1182
|
-
const current = m.session.model;
|
|
1183
|
-
const fresh = current && m.session.modelRuntime.getModel(current.provider, current.id);
|
|
1184
|
-
if (fresh && fresh !== current) m.session.agent.state.model = fresh;
|
|
1238
|
+
swapCurrentModelObject(m.session);
|
|
1185
1239
|
return true;
|
|
1186
1240
|
} catch (err) {
|
|
1187
1241
|
publish(m, { type: "error", message: `重载扩展失败:${String((err as Error)?.message ?? err)}`, ts: Date.now() });
|
|
@@ -1191,6 +1245,32 @@ async function reloadManagedResources(m: Managed): Promise<boolean> {
|
|
|
1191
1245
|
}
|
|
1192
1246
|
}
|
|
1193
1247
|
|
|
1248
|
+
/**
|
|
1249
|
+
* SDK 的 reload 不会换当前模型对象:models.json 改了 baseUrl / contextWindow 后,会话手里还是旧对象。
|
|
1250
|
+
* 按 provider/id 从(已就地刷新的)共享 runtime 取一次新的。
|
|
1251
|
+
*/
|
|
1252
|
+
function swapCurrentModelObject(session: AgentSession): void {
|
|
1253
|
+
const current = session.model;
|
|
1254
|
+
const fresh = current && session.modelRuntime.getModel(current.provider, current.id);
|
|
1255
|
+
if (fresh && fresh !== current) session.agent.state.model = fresh;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* 共享目录刷新后,给正在用这个模型的会话换上新模型对象(上下文窗口等字段跟着变),再推一次用量让圆环当场更新。
|
|
1260
|
+
* 运行中的会话也换:SDK 在扩展注册 provider 时同样会中途替换 agent.state.model,已发出的请求不受影响。
|
|
1261
|
+
*/
|
|
1262
|
+
export function refreshSessionModels(provider: string, id: string): number {
|
|
1263
|
+
let n = 0;
|
|
1264
|
+
for (const m of sessions.values()) {
|
|
1265
|
+
const current = m.session?.model;
|
|
1266
|
+
if (!m.session || current?.provider !== provider || current.id !== id) continue;
|
|
1267
|
+
swapCurrentModelObject(m.session);
|
|
1268
|
+
publishUsage(m);
|
|
1269
|
+
n += 1;
|
|
1270
|
+
}
|
|
1271
|
+
return n;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1194
1274
|
export async function reloadSessionsForCwd(cwd?: string): Promise<number> {
|
|
1195
1275
|
const key = (p: string) => (process.platform === "win32" ? p.replace(/\\/g, "/").toLowerCase() : p.replace(/\\/g, "/"));
|
|
1196
1276
|
let n = 0;
|
|
@@ -1198,7 +1278,7 @@ export async function reloadSessionsForCwd(cwd?: string): Promise<number> {
|
|
|
1198
1278
|
if (cwd && key(m.cwd) !== key(cwd)) continue;
|
|
1199
1279
|
if (!m.session) {
|
|
1200
1280
|
// 冷会话没有扩展运行时可重建,但页面可能开着:把新清单(模板 / 技能 / 信任)推过去
|
|
1201
|
-
if (m
|
|
1281
|
+
if (hasViewers(m)) publishResources(m);
|
|
1202
1282
|
continue;
|
|
1203
1283
|
}
|
|
1204
1284
|
if (m.creating || isManagedBusy(m)) {
|
|
@@ -1294,42 +1374,13 @@ export async function buildSnapshot(m: Managed): Promise<WebSnapshot> {
|
|
|
1294
1374
|
const usage = session.getContextUsage() ?? null;
|
|
1295
1375
|
contextUsage = usage ? { tokens: usage.tokens, contextWindow: usage.contextWindow, percent: usage.percent } : null;
|
|
1296
1376
|
} else if (model) {
|
|
1297
|
-
//
|
|
1298
|
-
// 上下文规模)除以模型窗口估算,避免上下文计量永远显示 0% / —
|
|
1377
|
+
// 冷会话也使用当前分支投影,包含最后一次 API 用量之后的用户消息和工具输出。
|
|
1299
1378
|
try {
|
|
1300
1379
|
const rt = await getModelRuntime();
|
|
1301
1380
|
const modelMeta = (rt.getModels(model.provider) as any[]).find((x) => String(x.id) === model.id);
|
|
1302
|
-
|
|
1303
|
-
let lastTurnTokens = 0;
|
|
1304
|
-
// 与 SDK getContextUsage 同一条规则:最近一次压缩之后还没有新的助手回复时,上下文大小未知(不能拿压缩前的用量充数)
|
|
1305
|
-
const entries = m.sm.getEntries() as Array<{ type?: string; message?: { role?: string; stopReason?: string } }>;
|
|
1306
|
-
let compactedSinceLastAssistant = false;
|
|
1307
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
1308
|
-
const e = entries[i];
|
|
1309
|
-
if (e.type === "compaction" || e.type === "branch_summary") {
|
|
1310
|
-
compactedSinceLastAssistant = true;
|
|
1311
|
-
break;
|
|
1312
|
-
}
|
|
1313
|
-
if (e.type === "message" && e.message?.role === "assistant" && e.message.stopReason !== "error" && e.message.stopReason !== "aborted") break;
|
|
1314
|
-
}
|
|
1315
|
-
for (let i = messages.length - 1; i >= 0 && !compactedSinceLastAssistant; i--) {
|
|
1316
|
-
const msg = messages[i];
|
|
1317
|
-
if (msg.role === "assistant" && msg.usage) {
|
|
1318
|
-
lastTurnTokens =
|
|
1319
|
-
Number(msg.usage.input ?? 0) +
|
|
1320
|
-
Number(msg.usage.cacheRead ?? 0) +
|
|
1321
|
-
Number(msg.usage.cacheWrite ?? 0) +
|
|
1322
|
-
Number(msg.usage.output ?? 0);
|
|
1323
|
-
break;
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
if (window > 0 && lastTurnTokens > 0) {
|
|
1327
|
-
contextUsage = { tokens: lastTurnTokens, contextWindow: window, percent: (lastTurnTokens / window) * 100 };
|
|
1328
|
-
} else if (window > 0) {
|
|
1329
|
-
contextUsage = { tokens: null, contextWindow: window, percent: null };
|
|
1330
|
-
}
|
|
1381
|
+
contextUsage = projectedContextUsage(m.sm.buildSessionProjection(), m.sm.getBranch(), Number(modelMeta?.contextWindow ?? 0));
|
|
1331
1382
|
} catch {
|
|
1332
|
-
/*
|
|
1383
|
+
/* 模型或投影不可用时用量未知。 */
|
|
1333
1384
|
}
|
|
1334
1385
|
}
|
|
1335
1386
|
|
|
@@ -1359,7 +1410,7 @@ export async function buildSnapshot(m: Managed): Promise<WebSnapshot> {
|
|
|
1359
1410
|
// 服务端可得的上下文分项(估算;失败不影响快照)
|
|
1360
1411
|
let contextBreakdown: ContextBreakdown | undefined;
|
|
1361
1412
|
try {
|
|
1362
|
-
contextBreakdown = await buildContextBreakdown(m
|
|
1413
|
+
contextBreakdown = await buildContextBreakdown(m, contextResources);
|
|
1363
1414
|
} catch {
|
|
1364
1415
|
contextBreakdown = undefined;
|
|
1365
1416
|
}
|
|
@@ -1446,6 +1497,18 @@ export function unsubscribe(m: Managed, send: (seq: number, json: string) => voi
|
|
|
1446
1497
|
m.subscribers.delete(send);
|
|
1447
1498
|
}
|
|
1448
1499
|
|
|
1500
|
+
/**
|
|
1501
|
+
* 长轮询(快速隧道)的一次请求结束时调用:下一次请求到来前的这段空档仍算有人在看,
|
|
1502
|
+
* 否则扩展弹窗会被当成无人值守直接取默认值,会话也可能被当成没人开着回收掉。
|
|
1503
|
+
*/
|
|
1504
|
+
export function holdViewer(m: Managed, ms: number): void {
|
|
1505
|
+
m.viewerGraceUntil = Math.max(m.viewerGraceUntil ?? 0, Date.now() + ms);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
function hasViewers(m: Managed): boolean {
|
|
1509
|
+
return m.subscribers.size > 0 || (m.viewerGraceUntil ?? 0) > Date.now();
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1449
1512
|
// ---------- 命令执行 ----------
|
|
1450
1513
|
|
|
1451
1514
|
export interface CommandResult {
|
|
@@ -1557,6 +1620,8 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1557
1620
|
if (!model) return { ok: false, error: `unknown model ${cmd.provider}/${cmd.modelId}` };
|
|
1558
1621
|
await session.setModel(model, { persist: false });
|
|
1559
1622
|
reapplyDesiredLevel(m, session);
|
|
1623
|
+
// 新会话页选的模型(fresh):思考档位按新会话的规则定;之后显式选的档位仍会覆盖
|
|
1624
|
+
if (cmd.fresh === true && !m.desiredThinkingLevel) applyNewSessionThinking(m, session);
|
|
1560
1625
|
publishModelState(m, session);
|
|
1561
1626
|
return { ok: true, data: { thinkingLevel: session.thinkingLevel, thinkingLevels: session.getAvailableThinkingLevels() } };
|
|
1562
1627
|
}
|
|
@@ -1669,8 +1734,17 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1669
1734
|
}
|
|
1670
1735
|
case "reload": {
|
|
1671
1736
|
if (isManagedBusy(m)) return { ok: false, error: "session is busy" };
|
|
1737
|
+
// /reload:先就地刷新共享的模型目录(models.json / auth.json 可能是手改的);其它会话手里的
|
|
1738
|
+
// 模型对象跟着换新,忙着的等本轮结束再换;本会话重载资源后再换一次
|
|
1739
|
+
await refreshModelRuntime();
|
|
1740
|
+
for (const other of sessions.values()) {
|
|
1741
|
+
if (other === m || !other.session) continue;
|
|
1742
|
+
if (isManagedBusy(other)) other.resourceReloadPending = true;
|
|
1743
|
+
else swapCurrentModelObject(other.session);
|
|
1744
|
+
}
|
|
1672
1745
|
const session = await ensureSession(m);
|
|
1673
1746
|
await session.reload();
|
|
1747
|
+
swapCurrentModelObject(session);
|
|
1674
1748
|
return { ok: true };
|
|
1675
1749
|
}
|
|
1676
1750
|
case "fork": {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话事件的长轮询通道(浏览器端)。
|
|
3
|
+
*
|
|
4
|
+
* Cloudflare 快速隧道(远程连接的公网入口)不支持 SSE:流式响应被攒成 128KB 一块才转发,
|
|
5
|
+
* 不满一块的尾巴一直卡着——打开会话等不到完整快照(一直转圈),聊天也不流式。
|
|
6
|
+
* 隧道下改成一次次短请求:服务端把这一批事件发完就结束响应(结束时隧道一定吐干净),
|
|
7
|
+
* 浏览器带着 Last-Event-ID 立刻再来一次,漏掉的事件由服务端从环形缓冲补发。
|
|
8
|
+
* 线上格式仍是 SSE 文本,服务端两种模式共用一套写法。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** 只有快速隧道需要轮询;局域网、本机、用户自己的反向代理都照旧走 SSE */
|
|
12
|
+
export function usesEventPolling(hostname: string): boolean {
|
|
13
|
+
return hostname.toLowerCase().endsWith(".trycloudflare.com");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SseFrame {
|
|
17
|
+
id?: string;
|
|
18
|
+
event?: string;
|
|
19
|
+
data: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 解析一段完整的 SSE 文本(只认本项目用到的 id / event / data,注释行跳过) */
|
|
23
|
+
export function parseSseText(text: string): SseFrame[] {
|
|
24
|
+
const frames: SseFrame[] = [];
|
|
25
|
+
for (const block of text.split(/\r?\n\r?\n/)) {
|
|
26
|
+
const frame: SseFrame = { data: "" };
|
|
27
|
+
const data: string[] = [];
|
|
28
|
+
for (const line of block.split(/\r?\n/)) {
|
|
29
|
+
if (!line || line.startsWith(":")) continue;
|
|
30
|
+
const colon = line.indexOf(":");
|
|
31
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
32
|
+
let value = colon === -1 ? "" : line.slice(colon + 1);
|
|
33
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
34
|
+
if (field === "data") data.push(value);
|
|
35
|
+
else if (field === "id") frame.id = value;
|
|
36
|
+
else if (field === "event") frame.event = value;
|
|
37
|
+
}
|
|
38
|
+
if (!data.length) continue;
|
|
39
|
+
frame.data = data.join("\n");
|
|
40
|
+
frames.push(frame);
|
|
41
|
+
}
|
|
42
|
+
return frames;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface EventPollHandlers {
|
|
46
|
+
/** 一次请求成功返回(断开后恢复时也会再调) */
|
|
47
|
+
onOpen(): void;
|
|
48
|
+
onMessage(data: string): void;
|
|
49
|
+
/** 一次请求失败;之后按退避自动重试 */
|
|
50
|
+
onError(): void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface EventPollOptions {
|
|
54
|
+
fetch?: typeof fetch;
|
|
55
|
+
/** 第 n 次连续失败后等多久再试 */
|
|
56
|
+
backoffMs?: (failures: number) => number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const defaultBackoff = (failures: number) => Math.min(2000 * 2 ** (failures - 1), 30_000);
|
|
60
|
+
|
|
61
|
+
export function pollEventStream(url: string, handlers: EventPollHandlers, { fetch: fetchImpl = fetch, backoffMs = defaultBackoff }: EventPollOptions = {}): { close(): void } {
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
let closed = false;
|
|
64
|
+
let lastEventId: string | undefined;
|
|
65
|
+
let open = false;
|
|
66
|
+
let failures = 0;
|
|
67
|
+
const pollUrl = `${url}${url.includes("?") ? "&" : "?"}poll=1`;
|
|
68
|
+
|
|
69
|
+
const sleep = (ms: number) => new Promise<void>((resolve) => {
|
|
70
|
+
const timer = setTimeout(resolve, ms);
|
|
71
|
+
controller.signal.addEventListener("abort", () => { clearTimeout(timer); resolve(); }, { once: true });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
void (async () => {
|
|
75
|
+
while (!closed) {
|
|
76
|
+
let frames: SseFrame[];
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetchImpl(pollUrl, {
|
|
79
|
+
headers: lastEventId ? { "Last-Event-ID": lastEventId } : {},
|
|
80
|
+
cache: "no-store",
|
|
81
|
+
signal: controller.signal,
|
|
82
|
+
});
|
|
83
|
+
if (!response.ok) throw new Error(`events poll failed (${response.status})`);
|
|
84
|
+
frames = parseSseText(await response.text());
|
|
85
|
+
} catch {
|
|
86
|
+
if (closed) return;
|
|
87
|
+
open = false;
|
|
88
|
+
handlers.onError();
|
|
89
|
+
failures += 1;
|
|
90
|
+
await sleep(backoffMs(failures));
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (closed) return;
|
|
94
|
+
failures = 0;
|
|
95
|
+
if (!open) {
|
|
96
|
+
open = true;
|
|
97
|
+
handlers.onOpen();
|
|
98
|
+
}
|
|
99
|
+
for (const frame of frames) {
|
|
100
|
+
if (closed) return;
|
|
101
|
+
handlers.onMessage(frame.data);
|
|
102
|
+
if (frame.id !== undefined) lastEventId = frame.id;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
})();
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
close() {
|
|
109
|
+
closed = true;
|
|
110
|
+
controller.abort();
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|