@rexvane/piweb 0.3.24 → 0.3.26
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 +11 -11
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +5 -5
- package/.next/react-loadable-manifest.json +13 -20
- package/.next/required-server-files.js +2 -1
- package/.next/required-server-files.json +2 -1
- package/.next/routes-manifest.json +6 -8
- package/.next/server/app/_global-error/page.js +2 -2
- 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.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js +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.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -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]/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/agent/resources/route.js +1 -0
- package/.next/server/app/api/agent/resources/route.js.nft.json +1 -0
- package/.next/server/app/api/agent/resources/route_client-reference-manifest.js +1 -0
- 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 +4 -4
- package/.next/server/app/api/github/route.js.nft.json +1 -1
- 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/models/defaults/route.js +1 -1
- package/.next/server/app/api/models/defaults/route.js.nft.json +1 -1
- 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 -3
- 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/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/index.html +1 -1
- package/.next/server/app/index.rsc +6 -7
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +6 -7
- package/.next/server/app/index.segments/_full.segment.rsc +6 -7
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -3
- package/.next/server/app/login/page.js +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.rsc +3 -3
- package/.next/server/app/login.segments/_full.segment.rsc +3 -3
- package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +3 -3
- 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 +11 -11
- package/.next/server/chunks/3924.js +3 -3
- package/.next/server/chunks/538.js +1 -1
- package/.next/server/chunks/620.js +1 -0
- package/.next/server/chunks/6229.js +1 -1
- package/.next/server/chunks/7496.js +1 -1
- package/.next/server/chunks/7662.js +2 -2
- package/.next/server/chunks/8686.js +1 -1
- package/.next/server/functions-config-manifest.json +1 -2
- 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/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/preview-props.json +3 -3
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/QJCtqgDKzGS2BdKABd9ch/_buildManifest.js +1 -0
- package/.next/static/chunks/2577.86c8538fbf05a8b2.js +1 -0
- package/.next/static/chunks/2716.fa561519675a3ff1.js +1 -0
- package/.next/static/chunks/2906.8a0e09bb47605cf1.js +1 -0
- package/.next/static/chunks/4673.ea36e55005c0f82f.js +1 -0
- package/.next/static/chunks/4686-4dc565817a160351.js +1 -0
- package/.next/static/chunks/4778.0d2c5d75b791d352.js +1 -0
- package/.next/static/chunks/{618.949981da99be7182.js → 618.9a22656b863f242b.js} +2 -2
- package/.next/static/chunks/app/_global-error/page-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/events/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/tree/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/new/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/resources/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/config/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/files/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/fs/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/git/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/github/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/growth/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/health/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/models/defaults/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/models/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/pi-settings/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/plugins/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/prompts/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/remote/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/security/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/export/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/sessions/import/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/sessions/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/skills/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/update/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/version/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/web-auth/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/workspaces/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/page-71f2d3b3ed864b3d.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/webpack-ffc09b5fcc8aaa6d.js +1 -0
- package/.next/static/css/014eb1f21414e615.css +1 -0
- package/.next/static/css/56b129a13637fcfd.css +1 -0
- package/.next/static/css/7e3caf797e4cb19a.css +1 -0
- package/.next/types/app/api/agent/{[id]/side-chat → resources}/route.ts +3 -3
- package/.next/types/routes.d.ts +2 -2
- package/.next/types/validator.ts +9 -9
- package/README.md +1 -1
- package/next.config.ts +1 -1
- package/package.json +9 -9
- package/scripts/apply-global.mjs +87 -0
- package/scripts/launcher.mjs +21 -0
- package/scripts/testing/prepare-sdk-tools.mjs +19 -0
- package/src/app/api/agent/new/route.ts +2 -1
- package/src/app/api/agent/resources/route.ts +18 -0
- package/src/app/api/models/route.ts +22 -1
- package/src/app/api/plugins/route.ts +3 -2
- package/src/app/api/prompts/route.ts +3 -2
- package/src/app/api/remote/route.ts +22 -4
- package/src/app/api/sessions/[id]/export/route.ts +3 -1
- package/src/app/api/skills/route.ts +3 -2
- package/src/app/globals.css +1002 -1765
- package/src/components/AppShell.tsx +128 -93
- package/src/components/ChatInput.tsx +177 -98
- package/src/components/ChatWindow.tsx +189 -56
- package/src/components/ContextMeter.tsx +33 -9
- package/src/components/CustomSelect.module.css +142 -0
- package/src/components/CustomSelect.tsx +157 -0
- package/src/components/DrawerPanel.tsx +1 -1
- package/src/components/ExtensionUI.tsx +9 -3
- package/src/components/FileViewer.tsx +24 -21
- package/src/components/GitPanel.tsx +35 -13
- package/src/components/GrowthTree.tsx +6 -2
- package/src/components/ImageViewer.tsx +72 -0
- package/src/components/MarkdownBlock.tsx +67 -96
- package/src/components/ModelSelector.tsx +36 -39
- package/src/components/PiMark.tsx +76 -41
- package/src/components/PreviewImage.tsx +33 -0
- package/src/components/ProjectPanel.tsx +8 -5
- package/src/components/PromptPanel.tsx +85 -20
- package/src/components/ProviderSetupModal.module.css +12 -11
- package/src/components/ProviderSetupModal.tsx +107 -36
- package/src/components/RemoteSection.tsx +53 -2
- package/src/components/SessionImportSection.tsx +30 -15
- package/src/components/SessionSidebar.tsx +72 -62
- package/src/components/SettingsPanel.tsx +49 -151
- package/src/components/TrajectoryAnalyzer.module.css +26 -11
- package/src/components/TrajectoryView.tsx +11 -3
- package/src/components/chat/markdown-actions.tsx +23 -0
- package/src/components/composer/ComposerFiles.tsx +224 -0
- package/src/components/composer/ComposerImages.tsx +59 -127
- package/src/hooks/usePiWeb.ts +47 -2
- package/src/i18n.tsx +62 -80
- package/src/lib/agent-manager.ts +365 -33
- package/src/lib/chat/file-path.ts +54 -0
- package/src/lib/chat/skill-expansion.ts +51 -0
- package/src/lib/extension-ui.ts +25 -1
- package/src/lib/font.ts +37 -39
- package/src/lib/git-service.ts +4 -2
- package/src/lib/growth-turns.ts +3 -1
- package/src/lib/model-draft.ts +69 -2
- package/src/lib/models-service.ts +53 -3
- package/src/lib/pi.ts +2 -2
- package/src/lib/remote/assets.json +1 -1
- package/src/lib/remote/supervisor.ts +48 -5
- package/src/lib/security-service.ts +2 -1
- package/src/lib/session-reader.ts +25 -13
- package/src/lib/session-retention.ts +11 -8
- package/src/lib/skills-service.ts +2 -1
- package/src/lib/slash-command-token.ts +14 -0
- package/src/lib/trajectory.ts +111 -15
- package/src/lib/types.ts +28 -3
- package/src/lib/update-service.ts +29 -9
- package/src/lib/workspace-store.ts +36 -1
- package/.next/server/app/api/agent/[id]/side-chat/route.js +0 -5
- package/.next/server/app/api/agent/[id]/side-chat/route.js.nft.json +0 -1
- package/.next/server/app/api/agent/[id]/side-chat/route_client-reference-manifest.js +0 -1
- package/.next/server/chunks/3421.js +0 -1
- package/.next/static/chunks/2716.44fdf1b21454b961.js +0 -1
- package/.next/static/chunks/4673.e261707dce391be7.js +0 -1
- package/.next/static/chunks/4686-0cd87513f9ea3538.js +0 -1
- package/.next/static/chunks/4778.eddd3a5813f85824.js +0 -1
- package/.next/static/chunks/7411.5f79671208209b13.js +0 -1
- package/.next/static/chunks/8552.25c39ef4c995eba0.js +0 -1
- package/.next/static/chunks/app/_global-error/page-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/events/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/side-chat/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/tree/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/new/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/config/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/files/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/fs/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/git/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/github/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/growth/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/health/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/models/defaults/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/models/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/pi-settings/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/plugins/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/prompts/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/remote/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/security/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/export/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/sessions/import/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/skills/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/update/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/version/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/web-auth/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/workspaces/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/page-dc3cb41d91c7432e.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/webpack-2ac3d76b81895f5b.js +0 -1
- package/.next/static/css/08815507594e8b8d.css +0 -1
- package/.next/static/css/9fc559c2c4ad1d0f.css +0 -1
- package/.next/static/css/c6e2227c36ec2fd6.css +0 -1
- package/.next/static/css/f15a853af8b5127f.css +0 -1
- package/.next/static/lvKCJmjFj8LuqSojRVLRF/_buildManifest.js +0 -1
- package/src/app/api/agent/[id]/side-chat/route.ts +0 -77
- package/src/components/side-chat/SideChatPanel.module.css +0 -131
- package/src/components/side-chat/SideChatPanel.tsx +0 -189
- package/src/components/side-chat/icons.tsx +0 -8
- package/src/components/side-chat/request-history.ts +0 -29
- package/src/components/side-chat/store.ts +0 -248
- package/src/lib/chat/stream-settings.ts +0 -192
- package/src/lib/side-chat/context.ts +0 -123
- package/src/lib/side-chat/service.ts +0 -191
- package/src/lib/side-chat/types.ts +0 -61
- /package/.next/static/{lvKCJmjFj8LuqSojRVLRF → QJCtqgDKzGS2BdKABd9ch}/_ssgManifest.js +0 -0
package/src/lib/agent-manager.ts
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* agent-manager:AgentSession 池 + pi 事件 → Web 事件翻译 + SSE 订阅管理。
|
|
3
3
|
* 打开会话即惰性冷启动(createAgentSession),空闲回收,事件先快照后增量。
|
|
4
4
|
*/
|
|
5
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
5
|
+
import { closeSync, existsSync, openSync, readFileSync, readSync, 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 { formatSkillsForPrompt, generateUnifiedPatch } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { ExtensionRunner, ModelRegistry, formatSkillsForPrompt, generateUnifiedPatch } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import {
|
|
11
11
|
getResourceLoader,
|
|
12
12
|
createSessionResourceLoader,
|
|
@@ -32,7 +32,7 @@ import { createExtensionUiBridge, type ExtensionUiBridge } from "./extension-ui"
|
|
|
32
32
|
import { createGrowthTracker, GROWTH_TRACKER_VERSION, type GrowthTracker } from "./growth-tracker";
|
|
33
33
|
import { userTurnsFromEntries } from "./growth-turns";
|
|
34
34
|
import { projectedContextUsage } from "./session/metrics";
|
|
35
|
-
import { TrajLedger, buildTrajectoryFromEntries, toTrajTokens } from "./trajectory";
|
|
35
|
+
import { TrajLedger, buildTrajectoryFromEntries, toolStepPreview, toTrajTokens } from "./trajectory";
|
|
36
36
|
import { startArchivedSessionRetention } from "./session-retention";
|
|
37
37
|
import { sanitizeToolOutput } from "./text-sanitize";
|
|
38
38
|
import { BoundaryError } from "./path-security";
|
|
@@ -56,8 +56,11 @@ const IDLE_MS = 10 * 60 * 1000;
|
|
|
56
56
|
const MAX_ACTIVE = 6;
|
|
57
57
|
const BUFFER_CAP = 400;
|
|
58
58
|
const SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
59
|
+
/** 外来写入重载后,这么久之内连上的快照都带着提示 */
|
|
60
|
+
const EXTERNAL_RELOAD_NOTICE_MS = 60_000;
|
|
59
61
|
|
|
60
62
|
interface Managed {
|
|
63
|
+
viewerAvailable?: () => boolean;
|
|
61
64
|
sessionPath: string;
|
|
62
65
|
cwd: string;
|
|
63
66
|
sm: SessionManager;
|
|
@@ -105,6 +108,12 @@ interface Managed {
|
|
|
105
108
|
ui: ExtensionUiBridge | null;
|
|
106
109
|
/** 项目生长:影子仓库快照的触发器(首个会话事件时创建,dispose 时释放) */
|
|
107
110
|
growth: GrowthTracker | null;
|
|
111
|
+
/** JSONL 已核对到的字节位置(总在整行末尾);之后多出来的行要看是不是别的进程写的(见 externalEntryCount) */
|
|
112
|
+
diskSize: number;
|
|
113
|
+
/** 已发现、还没重载进来的外来条目数(终端 pi 同时在写这个会话) */
|
|
114
|
+
externalEntries: number;
|
|
115
|
+
/** 这一份是因外来写入从磁盘重载出来的:快照带上,前端提示一次 */
|
|
116
|
+
externalReload?: { entries: number; at: number };
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
const globalForAgentManager = globalThis as typeof globalThis & {
|
|
@@ -164,10 +173,95 @@ export function toWebMessage(m: any, entryId?: string): WebMessage {
|
|
|
164
173
|
encodingLoss: result.encodingLoss || undefined,
|
|
165
174
|
patch: patchOf(m.details),
|
|
166
175
|
}];
|
|
176
|
+
} else if (m.role === "compactionSummary" || m.role === "branchSummary") {
|
|
177
|
+
out.role = "summary";
|
|
178
|
+
out.summaryKind = m.role === "branchSummary" ? "branch" : "compaction";
|
|
179
|
+
out.summary = typeof m.summary === "string" ? m.summary : "";
|
|
180
|
+
if (typeof m.tokensBefore === "number" && Number.isFinite(m.tokensBefore)) out.tokensBefore = m.tokensBefore;
|
|
167
181
|
}
|
|
168
182
|
return out;
|
|
169
183
|
}
|
|
170
184
|
|
|
185
|
+
/** 会话条目 → 显示消息:消息照常投影,压缩 / 分支摘要投影成摘要卡,其余条目(切模型、自定义状态等)不显示 */
|
|
186
|
+
function entryToWebMessage(entry: any): WebMessage | null {
|
|
187
|
+
if (entry?.type === "message" && entry.message) return toWebMessage(entry.message, entry.id);
|
|
188
|
+
if (entry?.type !== "compaction" && !(entry?.type === "branch_summary" && entry.summary)) return null;
|
|
189
|
+
const timestamp = Date.parse(entry.timestamp);
|
|
190
|
+
return toWebMessage({
|
|
191
|
+
role: entry.type === "compaction" ? "compactionSummary" : "branchSummary",
|
|
192
|
+
summary: entry.summary,
|
|
193
|
+
tokensBefore: entry.tokensBefore,
|
|
194
|
+
timestamp: Number.isFinite(timestamp) ? timestamp : undefined,
|
|
195
|
+
}, entry.id);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 被压缩掉的历史:当前分支上已不在模型上下文里的条目,按原顺序照常显示(可编辑、可分叉)。
|
|
200
|
+
* 每张压缩卡挪到它保留的第一条(firstKeptEntryId)前面,和上下文部分「摘要在前、保留消息在后」一致;
|
|
201
|
+
* 这样卡片以上就是模型只看得到摘要的内容。
|
|
202
|
+
*/
|
|
203
|
+
function compactedHistory(sm: Pick<SessionManager, "getBranch">, context: any[]): WebMessage[] {
|
|
204
|
+
if (context[0]?.type !== "compaction") return [];
|
|
205
|
+
const inContext = new Set(context.map((entry) => entry.id));
|
|
206
|
+
const entries = (sm.getBranch() as any[]).filter((entry) => !inContext.has(entry.id));
|
|
207
|
+
const position = new Map(entries.map((entry, index) => [entry.id, index]));
|
|
208
|
+
const markersBefore = new Map<string, WebMessage[]>();
|
|
209
|
+
const moved = new Set<string>();
|
|
210
|
+
entries.forEach((entry, index) => {
|
|
211
|
+
if (entry.type !== "compaction") return;
|
|
212
|
+
const at = position.get(entry.firstKeptEntryId);
|
|
213
|
+
if (at === undefined || at >= index) return;
|
|
214
|
+
const marker = entryToWebMessage(entry)!;
|
|
215
|
+
markersBefore.set(entry.firstKeptEntryId, [...(markersBefore.get(entry.firstKeptEntryId) ?? []), marker]);
|
|
216
|
+
moved.add(entry.id);
|
|
217
|
+
});
|
|
218
|
+
const out: WebMessage[] = [];
|
|
219
|
+
for (const entry of entries) {
|
|
220
|
+
out.push(...(markersBefore.get(entry.id) ?? []));
|
|
221
|
+
if (moved.has(entry.id)) continue;
|
|
222
|
+
const message = entryToWebMessage(entry);
|
|
223
|
+
if (message) out.push(message);
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** 冷会话 / 只读接口的消息列表:被压缩掉的历史 + 当前上下文(最新一次压缩投影成分隔卡) */
|
|
229
|
+
export function sessionDisplayMessages(sm: Pick<SessionManager, "getBranch" | "buildContextEntries">): WebMessage[] {
|
|
230
|
+
const context = sm.buildContextEntries() as any[];
|
|
231
|
+
const current = context.flatMap((entry, index) => {
|
|
232
|
+
// 保留区里更早的压缩条目不进模型上下文(SDK 投影只取下标 0 的那次),这里同样跳过
|
|
233
|
+
if (entry.type === "compaction" && index > 0) return [];
|
|
234
|
+
const message = entryToWebMessage(entry);
|
|
235
|
+
return message ? [message] : [];
|
|
236
|
+
});
|
|
237
|
+
return [...compactedHistory(sm, context), ...current];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* 压缩刚完成(SDK 先写条目再发 compaction_end):分隔卡 + 它该插在哪条消息前。
|
|
242
|
+
* 取保留下来的第一条用户 / 助手消息,和快照里「卡片在保留消息之前」的位置一致;没有保留消息就追加。
|
|
243
|
+
*/
|
|
244
|
+
function compactionMarker(m: Managed): { marker?: WebMessage; beforeId?: string; beforeStreamId?: string } {
|
|
245
|
+
try {
|
|
246
|
+
const context = m.sm.buildContextEntries() as any[];
|
|
247
|
+
if (context[0]?.type !== "compaction") return {};
|
|
248
|
+
const kept = context.find((entry) => entry.type === "message" && (entry.message?.role === "user" || entry.message?.role === "assistant"));
|
|
249
|
+
return {
|
|
250
|
+
marker: entryToWebMessage(context[0]) ?? undefined,
|
|
251
|
+
beforeId: kept?.id,
|
|
252
|
+
beforeStreamId: kept ? m.messageIds?.get(kept.message) : undefined,
|
|
253
|
+
};
|
|
254
|
+
} catch {
|
|
255
|
+
return {};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** 热会话里摘要消息(SDK 每次投影新建的对象)对应的条目 ID:压缩取上下文首条,分支摘要按 fromId + 时间匹配 */
|
|
260
|
+
function summaryEntryId(context: any[], message: any): string | undefined {
|
|
261
|
+
if (message?.role === "compactionSummary") return context[0]?.type === "compaction" ? context[0].id : undefined;
|
|
262
|
+
return context.find((entry) => entry.type === "branch_summary" && entry.fromId === message?.fromId && Date.parse(entry.timestamp) === message?.timestamp)?.id;
|
|
263
|
+
}
|
|
264
|
+
|
|
171
265
|
/** 消息对象 → 持久化条目 ID 的索引;条目数一变就重建(会话只追加,改写分支时条目数也会变) */
|
|
172
266
|
const entryIdIndexes = new WeakMap<SessionManager, { count: number; ids: WeakMap<object, string> }>();
|
|
173
267
|
|
|
@@ -345,7 +439,11 @@ function publishModelState(m: Managed, session: AgentSession): void {
|
|
|
345
439
|
});
|
|
346
440
|
}
|
|
347
441
|
|
|
348
|
-
/**
|
|
442
|
+
/**
|
|
443
|
+
* 切模型后恢复用户显式选过的级别(按新模型钳制)。
|
|
444
|
+
* 只记进本会话(SDK 会写 thinking_level_change 条目),不写全局默认:
|
|
445
|
+
* ~/.pi/agent/settings.json 与终端 pi 共用,在 PiWeb 里切一下档位不该改掉终端 pi 的默认值。
|
|
446
|
+
*/
|
|
349
447
|
function reapplyDesiredLevel(m: Managed, session: AgentSession): void {
|
|
350
448
|
if (!m.desiredThinkingLevel) return;
|
|
351
449
|
const target = clampThinkingLevel(session.model as any, m.desiredThinkingLevel as any);
|
|
@@ -406,6 +504,14 @@ function timingStats(m: Managed): Pick<WebStats, "llmMs" | "toolMs" | "ttftMs" |
|
|
|
406
504
|
return { llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens, llmTimedSteps, toolTimedCalls, decodeSteps };
|
|
407
505
|
}
|
|
408
506
|
|
|
507
|
+
/**
|
|
508
|
+
* 会话级用量缓存。SDK 的 getSessionStats() 每次都全表遍历 getEntries()(浅拷贝整个会话)、
|
|
509
|
+
* getContextUsage() 再 buildSessionProjection()(又一次全表拷贝 + 逐消息估算 token)——
|
|
510
|
+
* 一轮 agent 里 publishUsage 会在每个工具结束/每条消息结束各触发一次,长会话下这是
|
|
511
|
+
* 服务端最大的重复开销。条目数与 leaf 指针没变时结果必然相同,直接复用。
|
|
512
|
+
*/
|
|
513
|
+
const usageCache = new WeakMap<Managed, { entryCount: number; leafId: string | null; evt: Extract<WebEvent, { type: "usage" }> }>();
|
|
514
|
+
|
|
409
515
|
function sessionStats(m: Managed): WebStats | null {
|
|
410
516
|
if (!m.session) return null;
|
|
411
517
|
try {
|
|
@@ -459,17 +565,54 @@ function coldSessionStats(m: Managed): WebStats {
|
|
|
459
565
|
|
|
460
566
|
function usageEvent(m: Managed): WebEvent | null {
|
|
461
567
|
if (!m.session) return null;
|
|
568
|
+
// 条目数与 leaf 没变(还停在同一轮事件循环的同一状态)→ 复用上次的完整 usage 事件
|
|
569
|
+
try {
|
|
570
|
+
const entryCount = (m.sm as unknown as { getEntryCount?: () => number }).getEntryCount?.() ?? m.sm.getEntries().length;
|
|
571
|
+
const leafId = m.sm.getLeafId();
|
|
572
|
+
const cached = usageCache.get(m);
|
|
573
|
+
if (cached && cached.entryCount === entryCount && cached.leafId === leafId) return { ...cached.evt, ts: Date.now() };
|
|
574
|
+
} catch {
|
|
575
|
+
/* 缓存键拿不到就走原路径 */
|
|
576
|
+
}
|
|
462
577
|
const stats = sessionStats(m);
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
578
|
+
// 用量统计是旁路信息:SDK 偶发取不到时不能让调用方失败,
|
|
579
|
+
// 否则一次统计异常就会把「切模型 / 刷新目录」这类命令打回 ok:false。
|
|
580
|
+
let usage: ReturnType<AgentSession["getContextUsage"]> | null = null;
|
|
581
|
+
try {
|
|
582
|
+
usage = m.session.getContextUsage() ?? null;
|
|
583
|
+
} catch {
|
|
584
|
+
usage = null;
|
|
585
|
+
}
|
|
586
|
+
const evt = {
|
|
587
|
+
type: "usage" as const,
|
|
466
588
|
stats,
|
|
467
589
|
contextUsage: usage ? { tokens: usage.tokens, contextWindow: usage.contextWindow, percent: usage.percent } : null,
|
|
468
590
|
ts: Date.now(),
|
|
469
591
|
};
|
|
592
|
+
try {
|
|
593
|
+
const entryCount = (m.sm as unknown as { getEntryCount?: () => number }).getEntryCount?.() ?? m.sm.getEntries().length;
|
|
594
|
+
usageCache.set(m, { entryCount, leafId: m.sm.getLeafId(), evt });
|
|
595
|
+
} catch {
|
|
596
|
+
/* ignore */
|
|
597
|
+
}
|
|
598
|
+
return evt;
|
|
470
599
|
}
|
|
471
600
|
|
|
472
|
-
|
|
601
|
+
/**
|
|
602
|
+
* 忙时节流:一轮 agent 期间 publishUsage 的调用点密(每个工具结束 + 每条消息结束),
|
|
603
|
+
* 而 usage 在条目数没变时本来就不会动。500ms 内的重复请求直接跳过;
|
|
604
|
+
* settle/compaction 等关键节点用 flush 参数强制发一次,收尾数字不缺席。
|
|
605
|
+
*/
|
|
606
|
+
const USAGE_MIN_INTERVAL_MS = 500;
|
|
607
|
+
const usageThrottle = new WeakMap<Managed, number>();
|
|
608
|
+
|
|
609
|
+
function publishUsage(m: Managed, flush = false): void {
|
|
610
|
+
const now = Date.now();
|
|
611
|
+
if (!flush) {
|
|
612
|
+
const last = usageThrottle.get(m) ?? 0;
|
|
613
|
+
if (now - last < USAGE_MIN_INTERVAL_MS) return;
|
|
614
|
+
}
|
|
615
|
+
usageThrottle.set(m, now);
|
|
473
616
|
const evt = usageEvent(m);
|
|
474
617
|
if (evt) publish(m, evt);
|
|
475
618
|
}
|
|
@@ -591,7 +734,7 @@ function translate(m: Managed, evt: AgentSessionEvent): void {
|
|
|
591
734
|
case "agent_settled":
|
|
592
735
|
m.runActive = false;
|
|
593
736
|
publish(m, { type: "status", isStreaming: false, state: "idle", ts: now });
|
|
594
|
-
publishUsage(m);
|
|
737
|
+
publishUsage(m, true);
|
|
595
738
|
if (m.resourceReloadPending) queueMicrotask(() => void reloadManagedResources(m));
|
|
596
739
|
break;
|
|
597
740
|
case "queue_update":
|
|
@@ -612,9 +755,10 @@ function translate(m: Managed, evt: AgentSessionEvent): void {
|
|
|
612
755
|
phase: "end",
|
|
613
756
|
reason: (evt as any).reason,
|
|
614
757
|
errorMessage: (evt as any).errorMessage,
|
|
758
|
+
...((evt as any).result && !(evt as any).errorMessage ? compactionMarker(m) : {}),
|
|
615
759
|
ts: now,
|
|
616
760
|
});
|
|
617
|
-
publishUsage(m);
|
|
761
|
+
publishUsage(m, true);
|
|
618
762
|
publishContextBreakdown(m);
|
|
619
763
|
if ((evt as any).reason === "manual") {
|
|
620
764
|
publish(m, { type: "status", isStreaming: isManagedBusy(m), state: isManagedBusy(m) ? "running" : "idle", ts: now });
|
|
@@ -714,7 +858,8 @@ export function warmSessionResources(cwds: string[]): void {
|
|
|
714
858
|
|
|
715
859
|
export function getManaged(sessionPath: string): Managed {
|
|
716
860
|
let m = sessions.get(sessionPath);
|
|
717
|
-
if (
|
|
861
|
+
if (m) m = syncWithDisk(m);
|
|
862
|
+
else {
|
|
718
863
|
if (!existsSync(sessionPath)) throw new BoundaryError("session not found");
|
|
719
864
|
const sm = openSessionManager(sessionPath);
|
|
720
865
|
m = registerManaged(sessionPath, sm.getCwd() || process.cwd(), sm);
|
|
@@ -723,6 +868,115 @@ export function getManaged(sessionPath: string): Managed {
|
|
|
723
868
|
return m;
|
|
724
869
|
}
|
|
725
870
|
|
|
871
|
+
/**
|
|
872
|
+
* 数出 JSONL 里自上次核对以来、不是本进程写的新条目。
|
|
873
|
+
* 自己的条目在 SessionManager._appendEntry 里先进内存索引再落盘,所以索引里找不到 id 的行就是外来的
|
|
874
|
+
* (终端 pi 同时打开了这个会话)。只读新增的字节;文件变短(被整体重写)就从头核对;没写完的最后一行留到下次。
|
|
875
|
+
*/
|
|
876
|
+
function externalEntryCount(m: Managed): number {
|
|
877
|
+
let size: number;
|
|
878
|
+
try {
|
|
879
|
+
size = statSync(m.sessionPath).size;
|
|
880
|
+
} catch {
|
|
881
|
+
return 0; // 还没落盘
|
|
882
|
+
}
|
|
883
|
+
if (size === m.diskSize) return 0;
|
|
884
|
+
const from = size < m.diskSize ? 0 : m.diskSize;
|
|
885
|
+
let buf = Buffer.alloc(size - from);
|
|
886
|
+
let fd: number | undefined;
|
|
887
|
+
try {
|
|
888
|
+
fd = openSync(m.sessionPath, "r");
|
|
889
|
+
buf = buf.subarray(0, readSync(fd, buf, 0, buf.length, from));
|
|
890
|
+
} catch {
|
|
891
|
+
return 0;
|
|
892
|
+
} finally {
|
|
893
|
+
if (fd !== undefined) closeSync(fd);
|
|
894
|
+
}
|
|
895
|
+
const end = buf.lastIndexOf(0x0a);
|
|
896
|
+
m.diskSize = from + end + 1;
|
|
897
|
+
let count = 0;
|
|
898
|
+
for (const line of buf.subarray(0, Math.max(0, end)).toString("utf8").split("\n")) {
|
|
899
|
+
if (!line.trim()) continue;
|
|
900
|
+
try {
|
|
901
|
+
const entry = JSON.parse(line);
|
|
902
|
+
if (entry?.type !== "session" && typeof entry?.id === "string" && !m.sm.getEntry(entry.id)) count += 1;
|
|
903
|
+
} catch {
|
|
904
|
+
/* 从行中间读起的半行、写坏的行都不算 */
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return count;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* pi 终端和 PiWeb 共用同一份 JSONL,但各自在内存里记着会话树:一边写了,另一边不知道,
|
|
912
|
+
* 接着写就从自己的旧叶子分叉。打开会话、发消息都经过 getManaged,这里先核对磁盘:
|
|
913
|
+
* 有外来条目且本会话空闲,就丢掉内存里的旧副本从磁盘重开;正在跑就只提醒,空闲后的下一次核对再重载。
|
|
914
|
+
*/
|
|
915
|
+
function syncWithDisk(m: Managed): Managed {
|
|
916
|
+
const found = externalEntryCount(m);
|
|
917
|
+
if (!found && !m.externalEntries) return m;
|
|
918
|
+
m.externalEntries += found;
|
|
919
|
+
if (m.creating || isManagedBusy(m)) {
|
|
920
|
+
if (found) publish(m, { type: "external_write", entries: m.externalEntries, ts: Date.now() });
|
|
921
|
+
return m;
|
|
922
|
+
}
|
|
923
|
+
return reopenManaged(m);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/** 等同空闲回收后再打开:会话级的选择(工具档位、思考级别)带过去,正在看的浏览器立刻重连拿新快照 */
|
|
927
|
+
function reopenManaged(m: Managed): Managed {
|
|
928
|
+
let sm: SessionManager;
|
|
929
|
+
try {
|
|
930
|
+
sm = openSessionManager(m.sessionPath);
|
|
931
|
+
} catch {
|
|
932
|
+
return m; // 读不出来(正被写坏?)就先沿用旧副本,下次再试
|
|
933
|
+
}
|
|
934
|
+
const at = Date.now();
|
|
935
|
+
const reload = { entries: m.externalEntries, at };
|
|
936
|
+
// 直接写给订阅者:publish 可能排在队列里,晚于 dispose 就送不到了
|
|
937
|
+
const frame = JSON.stringify({ type: "external_reload", ...reload, ts: at });
|
|
938
|
+
for (const send of m.subscribers) {
|
|
939
|
+
try {
|
|
940
|
+
send(m.seq, frame);
|
|
941
|
+
} catch {
|
|
942
|
+
/* ignore */
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
disposeSession(m);
|
|
946
|
+
const fresh = registerManaged(m.sessionPath, sm.getCwd() || m.cwd, sm);
|
|
947
|
+
// 核对位置沿用旧的(整行末尾):打开时恰好没写完的那行,写完后还会被认作外来的
|
|
948
|
+
fresh.diskSize = m.diskSize;
|
|
949
|
+
fresh.toolPreset = m.toolPreset;
|
|
950
|
+
fresh.customActiveTools = m.customActiveTools;
|
|
951
|
+
fresh.desiredThinkingLevel = m.desiredThinkingLevel;
|
|
952
|
+
fresh.viewerGraceUntil = m.viewerGraceUntil;
|
|
953
|
+
fresh.externalReload = reload;
|
|
954
|
+
return fresh;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* 侧栏每 3 秒拉一次会话列表,顺带核对所有开着的会话:终端写入几秒内就能提示 / 重载,
|
|
959
|
+
* 回合中途发现的外来写入也在回合结束后的下一次拉取时重载(不在 agent_settled 里做:
|
|
960
|
+
* SDK 收尾时还可能有延后的提问要开跑,那一刻回收会把它丢掉)。每个会话一次 stat,没变就不读文件。
|
|
961
|
+
*/
|
|
962
|
+
export function syncSessionsFromDisk(): void {
|
|
963
|
+
for (const m of [...sessions.values()]) {
|
|
964
|
+
try {
|
|
965
|
+
syncWithDisk(m);
|
|
966
|
+
} catch {
|
|
967
|
+
/* 核对失败只是晚一点发现,打开 / 发送时还会再查 */
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function fileSize(file: string): number {
|
|
973
|
+
try {
|
|
974
|
+
return statSync(file).size;
|
|
975
|
+
} catch {
|
|
976
|
+
return 0;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
726
980
|
function registerManaged(sessionPath: string, cwd: string, sm: SessionManager): Managed {
|
|
727
981
|
let initialTrajectory: TrajEntry[] = [];
|
|
728
982
|
try {
|
|
@@ -754,6 +1008,8 @@ function registerManaged(sessionPath: string, cwd: string, sm: SessionManager):
|
|
|
754
1008
|
runActive: false,
|
|
755
1009
|
disposed: false,
|
|
756
1010
|
messageIds: new WeakMap(),
|
|
1011
|
+
diskSize: fileSize(sessionPath),
|
|
1012
|
+
externalEntries: 0,
|
|
757
1013
|
};
|
|
758
1014
|
sessions.set(sessionPath, m);
|
|
759
1015
|
return m;
|
|
@@ -872,6 +1128,10 @@ function publishContextBreakdown(m: Managed): void {
|
|
|
872
1128
|
});
|
|
873
1129
|
}
|
|
874
1130
|
|
|
1131
|
+
/** 资源加载器注入文件的展示上限:同一份全文已经随快照的 contextResources 下发过一次,
|
|
1132
|
+
* 轨迹条目的 detail 只留头部供列表预览与搜索——两处不再各带一份全文 */
|
|
1133
|
+
const TRAJ_CONTEXT_DETAIL_MAX_CHARS = 4_000;
|
|
1134
|
+
|
|
875
1135
|
/** resources 已经算过(快照)就传进来,免得资源发现结果再读一遍 */
|
|
876
1136
|
function environmentTrajectory(m: Managed, session: AgentSession | null, known?: ContextResource[]): TrajEntry[] {
|
|
877
1137
|
const ts = m.ledger.entries[0]?.ts ?? Date.now();
|
|
@@ -893,7 +1153,10 @@ function environmentTrajectory(m: Managed, session: AgentSession | null, known?:
|
|
|
893
1153
|
kind: "context",
|
|
894
1154
|
ts,
|
|
895
1155
|
title: resource.path,
|
|
896
|
-
|
|
1156
|
+
// 全文在快照 contextResources 里;这里只留头部,快照不再双份序列化
|
|
1157
|
+
detail: resource.content.length > TRAJ_CONTEXT_DETAIL_MAX_CHARS
|
|
1158
|
+
? `${resource.content.slice(0, TRAJ_CONTEXT_DETAIL_MAX_CHARS)}…`
|
|
1159
|
+
: resource.content,
|
|
897
1160
|
}));
|
|
898
1161
|
} catch {
|
|
899
1162
|
/* Resource discovery diagnostics are already surfaced by Pi. */
|
|
@@ -913,6 +1176,7 @@ export async function ensureSession(m: Managed): Promise<AgentSession> {
|
|
|
913
1176
|
if (m.session) return m.session;
|
|
914
1177
|
m.creating = (async () => {
|
|
915
1178
|
await resourceLoaderReady(m.cwd);
|
|
1179
|
+
const settingsManager = getSettingsManager(m.cwd);
|
|
916
1180
|
const loader = await createSessionResourceLoader(m.cwd);
|
|
917
1181
|
let created: AgentSession | undefined;
|
|
918
1182
|
try {
|
|
@@ -923,7 +1187,7 @@ export async function ensureSession(m: Managed): Promise<AgentSession> {
|
|
|
923
1187
|
sessionManager: m.sm,
|
|
924
1188
|
modelRuntime: await getModelRuntime(),
|
|
925
1189
|
resourceLoader: loader,
|
|
926
|
-
settingsManager
|
|
1190
|
+
settingsManager,
|
|
927
1191
|
});
|
|
928
1192
|
created = session;
|
|
929
1193
|
if (m.disposed) throw new Error("session is disposed");
|
|
@@ -1178,7 +1442,22 @@ startArchivedSessionRetention({ isActive: isSessionInUse, log: (message) => cons
|
|
|
1178
1442
|
|
|
1179
1443
|
type ResourceBundle = Pick<WebSnapshot, "skills" | "promptTemplates" | "extensionCommands" | "projectTrust" | "resourceDiagnostics">;
|
|
1180
1444
|
|
|
1181
|
-
|
|
1445
|
+
/**
|
|
1446
|
+
* 会话还不存在时也能发现命令:不绑定运行时、不发 session_start、不启动 MCP。
|
|
1447
|
+
* 新会话页和冷会话的斜杠补全都靠它,等第一次提问才建会话。
|
|
1448
|
+
*/
|
|
1449
|
+
export async function loadWorkspaceResources(cwd: string): Promise<ResourceBundle> {
|
|
1450
|
+
await refreshResourceLoaderIfStale(cwd);
|
|
1451
|
+
const modelRegistry = new ModelRegistry(await getModelRuntime());
|
|
1452
|
+
return withResourceLock(async () => {
|
|
1453
|
+
const loader = getResourceLoader(cwd);
|
|
1454
|
+
const { extensions, runtime } = loader.getExtensions();
|
|
1455
|
+
const runner = new ExtensionRunner(extensions, runtime, cwd, SessionManager.inMemory(cwd), modelRegistry);
|
|
1456
|
+
return collectResources({ cwd, session: null }, runner);
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
function collectResources(m: Pick<Managed, "cwd" | "session">, commandRunner?: ExtensionRunner): ResourceBundle {
|
|
1182
1461
|
const loader = m.session?.resourceLoader ?? peekResourceLoader(m.cwd) ?? getResourceLoader(m.cwd);
|
|
1183
1462
|
const diagnostics: WebSnapshot["resourceDiagnostics"] = [];
|
|
1184
1463
|
let skills: WebSnapshot["skills"] = [];
|
|
@@ -1203,9 +1482,9 @@ function collectResources(m: Managed): ResourceBundle {
|
|
|
1203
1482
|
/* ignore */
|
|
1204
1483
|
}
|
|
1205
1484
|
let extensionCommands: WebSnapshot["extensionCommands"] = [];
|
|
1206
|
-
if (m.session) {
|
|
1485
|
+
if (m.session || commandRunner) {
|
|
1207
1486
|
try {
|
|
1208
|
-
const runner =
|
|
1487
|
+
const runner = commandRunner ?? m.session?.extensionRunner;
|
|
1209
1488
|
if (runner) {
|
|
1210
1489
|
extensionCommands = runner.getRegisteredCommands().map((c) => ({
|
|
1211
1490
|
name: c.invocationName,
|
|
@@ -1235,6 +1514,13 @@ async function reloadManagedResources(m: Managed): Promise<boolean> {
|
|
|
1235
1514
|
m.resourceReloadPending = false;
|
|
1236
1515
|
try {
|
|
1237
1516
|
await m.session.reload();
|
|
1517
|
+
// pi 1.3.0 起 reload 会保留原来的活动工具集:被移除的扩展工具会继续留在活动集里。
|
|
1518
|
+
// 这里按当前注册表重算一次;策略本身出错不能把重载判成失败。
|
|
1519
|
+
try {
|
|
1520
|
+
applyToolPolicy(m, m.session);
|
|
1521
|
+
} catch {
|
|
1522
|
+
/* 工具策略是附带动作,失败下次重载/切档位会再算 */
|
|
1523
|
+
}
|
|
1238
1524
|
swapCurrentModelObject(m.session);
|
|
1239
1525
|
return true;
|
|
1240
1526
|
} catch (err) {
|
|
@@ -1265,7 +1551,9 @@ export function refreshSessionModels(provider: string, id: string): number {
|
|
|
1265
1551
|
const current = m.session?.model;
|
|
1266
1552
|
if (!m.session || current?.provider !== provider || current.id !== id) continue;
|
|
1267
1553
|
swapCurrentModelObject(m.session);
|
|
1268
|
-
|
|
1554
|
+
// 换了模型对象(contextWindow 可能变了):条目数没变也会命中旧缓存,先失效再发
|
|
1555
|
+
usageCache.delete(m);
|
|
1556
|
+
publishUsage(m, true);
|
|
1269
1557
|
n += 1;
|
|
1270
1558
|
}
|
|
1271
1559
|
return n;
|
|
@@ -1300,8 +1588,10 @@ export async function buildSnapshot(m: Managed): Promise<WebSnapshot> {
|
|
|
1300
1588
|
// 用户刚在 prompts/skills/extensions 目录新建了文件的话顺手重载一次(只 stat 几个目录)
|
|
1301
1589
|
await resourceLoaderReady(m.cwd);
|
|
1302
1590
|
try {
|
|
1303
|
-
//
|
|
1304
|
-
if (m.
|
|
1591
|
+
// 创建还未完成时,资源版本尚未落定;快照不能把它误标成需要再次 session_start。
|
|
1592
|
+
if (m.creating) {
|
|
1593
|
+
// 当前创建流程会发布最终资源状态。
|
|
1594
|
+
} else if (m.session && isManagedBusy(m)) {
|
|
1305
1595
|
if (resourceLoaderIsStale(m.cwd) || m.resourceVersion !== resourceLoaderVersion(m.cwd)) m.resourceReloadPending = true;
|
|
1306
1596
|
} else {
|
|
1307
1597
|
await refreshResourceLoaderIfStale(m.cwd);
|
|
@@ -1323,7 +1613,22 @@ export async function buildSnapshot(m: Managed): Promise<WebSnapshot> {
|
|
|
1323
1613
|
|
|
1324
1614
|
if (session) {
|
|
1325
1615
|
const hasInflightFinal = linkInflightFinal(m, session);
|
|
1326
|
-
|
|
1616
|
+
// session.messages 只是压缩后的模型上下文;前面补上被压缩掉的历史,压缩摘要投影成分隔卡
|
|
1617
|
+
let context: any[] = [];
|
|
1618
|
+
let history: WebMessage[] = [];
|
|
1619
|
+
try {
|
|
1620
|
+
context = m.sm.buildContextEntries() as any[];
|
|
1621
|
+
history = compactedHistory(m.sm, context);
|
|
1622
|
+
} catch {
|
|
1623
|
+
/* 条目读不到时只显示上下文 */
|
|
1624
|
+
}
|
|
1625
|
+
messages = [
|
|
1626
|
+
...history,
|
|
1627
|
+
...session.messages.map((message: any) =>
|
|
1628
|
+
message?.role === "compactionSummary" || message?.role === "branchSummary"
|
|
1629
|
+
? toWebMessage(message, summaryEntryId(context, message))
|
|
1630
|
+
: projectMessage(m, message)),
|
|
1631
|
+
];
|
|
1327
1632
|
// final 已被 SDK push 时只投影一次;延后的 end 使用相同 streamId 继续更新权威内容。
|
|
1328
1633
|
if (!hasInflightFinal && m.inflight) {
|
|
1329
1634
|
const partial = projectMessage(m, m.inflight);
|
|
@@ -1340,12 +1645,9 @@ export async function buildSnapshot(m: Managed): Promise<WebSnapshot> {
|
|
|
1340
1645
|
}
|
|
1341
1646
|
isStreaming = isManagedBusy(m);
|
|
1342
1647
|
} else {
|
|
1343
|
-
// 冷渲染:从 JSONL
|
|
1648
|
+
// 冷渲染:从 JSONL 还原消息(不启动 agent),压缩掉的历史也照常显示
|
|
1344
1649
|
try {
|
|
1345
|
-
|
|
1346
|
-
messages = entries
|
|
1347
|
-
.filter((e: any) => e.type === "message" && e.message)
|
|
1348
|
-
.map((e: any) => toWebMessage(e.message, e.id));
|
|
1650
|
+
messages = sessionDisplayMessages(m.sm);
|
|
1349
1651
|
} catch {
|
|
1350
1652
|
messages = [];
|
|
1351
1653
|
}
|
|
@@ -1416,7 +1718,8 @@ export async function buildSnapshot(m: Managed): Promise<WebSnapshot> {
|
|
|
1416
1718
|
}
|
|
1417
1719
|
|
|
1418
1720
|
// 斜杠命令数据源:技能 / 提示模板 / 扩展命令,加上项目信任与资源诊断
|
|
1419
|
-
|
|
1721
|
+
// 冷会话还没有 extensionRunner,但命令目录已经能发现;不要在第一次提问前推空列表。
|
|
1722
|
+
const resources = session && !m.creating ? collectResources(m) : await loadWorkspaceResources(m.cwd);
|
|
1420
1723
|
|
|
1421
1724
|
const baseTrajectory = m.ledger.entries.length ? m.ledger.entries : coldTrajectory(m);
|
|
1422
1725
|
const trajectory = [...environmentTrajectory(m, session, contextResourcesLoaded ? contextResources : undefined), ...baseTrajectory];
|
|
@@ -1448,6 +1751,8 @@ export async function buildSnapshot(m: Managed): Promise<WebSnapshot> {
|
|
|
1448
1751
|
queue,
|
|
1449
1752
|
trajectory,
|
|
1450
1753
|
isStreaming,
|
|
1754
|
+
// 重载时没人在看(打开会话那一刻才发现):靠快照把提示带给随后连上的浏览器,前端按 at 去重
|
|
1755
|
+
...(m.externalReload && Date.now() - m.externalReload.at < EXTERNAL_RELOAD_NOTICE_MS ? { externalReload: m.externalReload } : {}),
|
|
1451
1756
|
};
|
|
1452
1757
|
}
|
|
1453
1758
|
|
|
@@ -1506,6 +1811,7 @@ export function holdViewer(m: Managed, ms: number): void {
|
|
|
1506
1811
|
}
|
|
1507
1812
|
|
|
1508
1813
|
function hasViewers(m: Managed): boolean {
|
|
1814
|
+
if (m.viewerAvailable) return m.viewerAvailable();
|
|
1509
1815
|
return m.subscribers.size > 0 || (m.viewerGraceUntil ?? 0) > Date.now();
|
|
1510
1816
|
}
|
|
1511
1817
|
|
|
@@ -1528,6 +1834,17 @@ async function prepareGrowthBriefly(m: Managed): Promise<void> {
|
|
|
1528
1834
|
]);
|
|
1529
1835
|
}
|
|
1530
1836
|
|
|
1837
|
+
type PromptImages = NonNullable<NonNullable<Parameters<AgentSession["prompt"]>[1]>["images"]>;
|
|
1838
|
+
|
|
1839
|
+
/** 被编辑的那条用户消息里的图片:编辑只改文字,图片原样带进重发(编辑框里也照样显示它们) */
|
|
1840
|
+
function userMessageImages(session: AgentSession, entryId: string): PromptImages {
|
|
1841
|
+
const entry = session.sessionManager?.getEntry?.(entryId) as { type?: string; message?: { role?: string; content?: unknown } } | undefined;
|
|
1842
|
+
if (entry?.type !== "message" || entry.message?.role !== "user" || !Array.isArray(entry.message.content)) return [];
|
|
1843
|
+
return entry.message.content.filter(
|
|
1844
|
+
(c): c is PromptImages[number] => c?.type === "image" && typeof c.data === "string" && typeof c.mimeType === "string",
|
|
1845
|
+
);
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1531
1848
|
/** 交给 SDK,并在「受理」那一刻返回,而不是等整轮跑完;受理之后的错误经 SSE 可见 */
|
|
1532
1849
|
function submitPrompt(
|
|
1533
1850
|
m: Managed,
|
|
@@ -1549,7 +1866,10 @@ function submitPrompt(
|
|
|
1549
1866
|
});
|
|
1550
1867
|
// 同步挂好 rejection handler;接受后的错误仍经 SSE 可见,不能成为未处理拒绝。
|
|
1551
1868
|
void run.then(() => {
|
|
1552
|
-
|
|
1869
|
+
// 上一轮的 agent_settled 还没发完时,SDK 不拒绝提问,而是把它挂进 deferredSettledActions,
|
|
1870
|
+
// 立即返回并在稍后用同一份 options 重发;这条路径不会调用 preflightResult。
|
|
1871
|
+
// 消息确实会发出去,不能报「未受理」——否则界面报错,消息随后照样出现。真正的拒绝只会是 rejection。
|
|
1872
|
+
if (!accepted) resolve({ ok: true, data: { accepted: true } });
|
|
1553
1873
|
}, (error) => {
|
|
1554
1874
|
const message = String(error?.message ?? error);
|
|
1555
1875
|
if (accepted) publish(m, { type: "error", message, ts: Date.now() });
|
|
@@ -1622,7 +1942,9 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1622
1942
|
reapplyDesiredLevel(m, session);
|
|
1623
1943
|
// 新会话页选的模型(fresh):思考档位按新会话的规则定;之后显式选的档位仍会覆盖
|
|
1624
1944
|
if (cmd.fresh === true && !m.desiredThinkingLevel) applyNewSessionThinking(m, session);
|
|
1945
|
+
usageCache.delete(m); // 换模型 = contextWindow 变了,旧 usage 缓存作废
|
|
1625
1946
|
publishModelState(m, session);
|
|
1947
|
+
publishUsage(m, true);
|
|
1626
1948
|
return { ok: true, data: { thinkingLevel: session.thinkingLevel, thinkingLevels: session.getAvailableThinkingLevels() } };
|
|
1627
1949
|
}
|
|
1628
1950
|
case "setThinkingLevel": {
|
|
@@ -1631,6 +1953,7 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1631
1953
|
// 与 pi 终端一致:不支持的档位就近钳制而不是拒绝(非推理模型 → off),并把实际生效值回传
|
|
1632
1954
|
const effective = clampThinkingLevel(session.model as any, requested as any);
|
|
1633
1955
|
m.desiredThinkingLevel = requested;
|
|
1956
|
+
// 不传 persist:与终端 pi 一样,聊天里切档位只影响本会话;全局默认仍以 settings.json 为准(见 reapplyDesiredLevel)
|
|
1634
1957
|
session.setThinkingLevel(effective as never);
|
|
1635
1958
|
publishModelState(m, session);
|
|
1636
1959
|
return { ok: true, data: { requested, thinkingLevel: session.thinkingLevel, clamped: session.thinkingLevel !== requested } };
|
|
@@ -1638,7 +1961,8 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1638
1961
|
case "setToolPreset": {
|
|
1639
1962
|
const preset = cmd.preset as ToolPreset;
|
|
1640
1963
|
if (!Object.hasOwn(TOOL_PRESETS, preset)) return { ok: false, error: "invalid tool preset" };
|
|
1641
|
-
|
|
1964
|
+
// 运行中也能改:SDK 的 setActiveToolsByName 不做 busy 检查,改完重建系统提示词,
|
|
1965
|
+
// 在下一轮生效(本轮的请求已经发出去了,改不了)。这里不再自行拦一层。
|
|
1642
1966
|
if (!m.session && !m.creating) {
|
|
1643
1967
|
// 冷会话只记下选择,等真正启动时(ensureSession → applyToolPolicy)再生效。
|
|
1644
1968
|
// 不能为了切个档位就冷启动 agent:打开会话时前端会恢复本地档位,
|
|
@@ -1649,7 +1973,7 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1649
1973
|
return { ok: true };
|
|
1650
1974
|
}
|
|
1651
1975
|
const session = await ensureSession(m);
|
|
1652
|
-
if (
|
|
1976
|
+
if (m.disposed) return { ok: false, error: "session is disposed" };
|
|
1653
1977
|
m.toolPreset = preset;
|
|
1654
1978
|
m.customActiveTools = undefined;
|
|
1655
1979
|
applyToolPolicy(m, session);
|
|
@@ -1658,9 +1982,9 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1658
1982
|
return { ok: true };
|
|
1659
1983
|
}
|
|
1660
1984
|
case "setActiveTools": {
|
|
1661
|
-
|
|
1985
|
+
// 同上:运行中允许改,下一轮生效。
|
|
1662
1986
|
const session = await ensureSession(m);
|
|
1663
|
-
if (
|
|
1987
|
+
if (m.disposed) return { ok: false, error: "session is disposed" };
|
|
1664
1988
|
const wanted = Array.isArray(cmd.names) ? cmd.names.map(String) : [];
|
|
1665
1989
|
const all = listAllToolNames(session);
|
|
1666
1990
|
const next = [...new Set(wanted)].filter((n) => all.includes(n));
|
|
@@ -1708,13 +2032,15 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1708
2032
|
}
|
|
1709
2033
|
}
|
|
1710
2034
|
if (m.disposed) throw new Error("session is disposed");
|
|
2035
|
+
// 回到消息之前先取它带的图片:客户端只发改过的文字,不取的话重发出去图就没了
|
|
2036
|
+
const images = userMessageImages(session, entryId);
|
|
1711
2037
|
const moved = await session.navigateTree(entryId);
|
|
1712
2038
|
if (moved?.cancelled) return { ok: false, error: "edit cancelled" };
|
|
1713
2039
|
await prepareGrowthBriefly(m);
|
|
1714
2040
|
if (m.disposed) throw new Error("session is disposed");
|
|
1715
2041
|
// 与普通 prompt 一样在受理时就返回:等整轮跑完的话,这期间 promptSubmitting 一直为真,
|
|
1716
2042
|
// 插话 / 排队 / 新消息全被拒,HTTP 请求也一直挂着
|
|
1717
|
-
return await submitPrompt(m, session, text);
|
|
2043
|
+
return await submitPrompt(m, session, text, images.length ? { images } : {});
|
|
1718
2044
|
} finally {
|
|
1719
2045
|
m.promptSubmitting = false;
|
|
1720
2046
|
if (!m.disposed) publish(m, { type: "status", isStreaming: isManagedBusy(m), state: isManagedBusy(m) ? "running" : "idle", ts: Date.now() });
|
|
@@ -1744,6 +2070,12 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1744
2070
|
}
|
|
1745
2071
|
const session = await ensureSession(m);
|
|
1746
2072
|
await session.reload();
|
|
2073
|
+
// 同 reloadManagedResources:pi 1.3.0 保留活动工具集,重载后按当前注册表重算
|
|
2074
|
+
try {
|
|
2075
|
+
applyToolPolicy(m, session);
|
|
2076
|
+
} catch {
|
|
2077
|
+
/* 同上:策略失败不影响 /reload 本身 */
|
|
2078
|
+
}
|
|
1747
2079
|
swapCurrentModelObject(session);
|
|
1748
2080
|
return { ok: true };
|
|
1749
2081
|
}
|
|
@@ -1784,7 +2116,7 @@ export function activeStatus(): Record<string, { streaming: boolean; lastStep?:
|
|
|
1784
2116
|
for (let i = m.ledger.entries.length - 1; i >= 0; i -= 1) {
|
|
1785
2117
|
const e = m.ledger.entries[i];
|
|
1786
2118
|
if (e.kind === "tool") {
|
|
1787
|
-
lastStep = [e.toolName, (e
|
|
2119
|
+
lastStep = [e.toolName, toolStepPreview(e)].filter(Boolean).join(" · ");
|
|
1788
2120
|
break;
|
|
1789
2121
|
}
|
|
1790
2122
|
}
|