@rexvane/piweb 0.3.21 → 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 +10 -10
- package/.next/required-server-files.js +16 -1
- package/.next/required-server-files.json +16 -1
- 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 +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/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 -1
- 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 -2
- package/.next/server/chunks/686.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/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/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/package.json +2 -2
- package/src/app/api/files/route.ts +5 -2
- package/src/app/api/github/route.ts +26 -0
- package/src/app/api/models/defaults/route.ts +18 -0
- package/src/app/api/models/route.ts +24 -1
- package/src/app/globals.css +186 -0
- package/src/components/AppShell.tsx +82 -30
- package/src/components/ChatInput.tsx +244 -63
- package/src/components/ChatWindow.tsx +18 -27
- package/src/components/ContextMeter.tsx +193 -143
- package/src/components/GitPanel.tsx +110 -40
- 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 +243 -27
- package/src/components/TrajectoryView.tsx +4 -6
- package/src/components/composer/ComposerImages.tsx +306 -0
- package/src/hooks/useGrowth.ts +3 -0
- package/src/hooks/usePiWeb.ts +12 -2
- package/src/hooks/useWorkspaceFiles.ts +93 -0
- package/src/i18n.tsx +108 -2
- package/src/lib/agent-manager.ts +135 -104
- 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 +12 -1
- package/src/lib/workspace-index.ts +270 -0
- package/.next/server/chunks/741.js +0 -165
- package/.next/static/chunks/316-5ea443ee6b512aa9.js +0 -1
- package/.next/static/chunks/3794-3679f1c05160977c.js +0 -52
- package/.next/static/chunks/4673.8a5a9e6855350e34.js +0 -1
- package/.next/static/chunks/4686-882531aef1d2e250.js +0 -1
- package/.next/static/chunks/4778.305dc6abb9889627.js +0 -1
- package/.next/static/chunks/4bd1b696-93bf506b236d6248.js +0 -1
- package/.next/static/chunks/618.ee5d5bf1c647e483.js +0 -4
- package/.next/static/chunks/839.bfd2db36483faa3e.js +0 -1
- package/.next/static/chunks/app/_global-error/page-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/events/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/tree/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/agent/new/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/config/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/files/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/fs/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/git/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/growth/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/health/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/models/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/pi-settings/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/plugins/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/prompts/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/remote/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/security/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/export/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/sessions/import/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/skills/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/update/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/version/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/web-auth/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/api/workspaces/route-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/app/page-c04af72e549695bd.js +0 -1
- package/.next/static/chunks/framework-c4e651fe89edfa91.js +0 -1
- package/.next/static/chunks/main-6159b22a8cb0b7c5.js +0 -5
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-b179d9f7f6f2daf9.js +0 -1
- package/.next/static/chunks/webpack-9b4bb574e5817654.js +0 -1
- package/.next/static/css/ea27b9e30f76303f.css +0 -1
- package/.next/static/sNX5GQkOWuK5YEID1hT-C/_buildManifest.js +0 -1
- /package/.next/static/{sNX5GQkOWuK5YEID1hT-C → WMazYvD-gbeLsCUfUxvvM}/_ssgManifest.js +0 -0
package/src/lib/agent-manager.ts
CHANGED
|
@@ -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 {
|
|
@@ -67,6 +68,8 @@ interface Managed {
|
|
|
67
68
|
viewerGraceUntil?: number;
|
|
68
69
|
/** 本轮已排队推送上下文分项(publishContextBreakdown 合并同一轮的多次触发) */
|
|
69
70
|
breakdownQueued?: boolean;
|
|
71
|
+
/** 首次运行后 pi 才会把系统分段持久化,届时用它把分类从差分换成投影口径,只换一次 */
|
|
72
|
+
breakdownProjected?: boolean;
|
|
70
73
|
buffer: { seq: number; json: string }[];
|
|
71
74
|
seq: number;
|
|
72
75
|
/**
|
|
@@ -349,36 +352,58 @@ function reapplyDesiredLevel(m: Managed, session: AgentSession): void {
|
|
|
349
352
|
if (target !== session.thinkingLevel) session.setThinkingLevel(target as never);
|
|
350
353
|
}
|
|
351
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
|
+
|
|
352
371
|
function publishTraj(m: Managed): void {
|
|
353
372
|
for (const entry of m.ledger.drainDirty()) {
|
|
354
373
|
publish(m, { type: "traj", entry, ts: Date.now() });
|
|
355
374
|
}
|
|
356
375
|
}
|
|
357
376
|
|
|
358
|
-
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"> {
|
|
359
378
|
let llmMs = 0;
|
|
360
379
|
let toolMs = 0;
|
|
361
380
|
let ttftMs = 0;
|
|
362
381
|
let ttftSteps = 0;
|
|
363
382
|
let decodeMs = 0;
|
|
364
383
|
let decodeTokens = 0;
|
|
384
|
+
let llmTimedSteps = 0;
|
|
385
|
+
let toolTimedCalls = 0;
|
|
386
|
+
let decodeSteps = 0;
|
|
365
387
|
for (const entry of m.ledger.entries) {
|
|
366
388
|
if (entry.kind === "tool") {
|
|
389
|
+
if (entry.timing?.durationMs !== undefined) toolTimedCalls++;
|
|
367
390
|
toolMs += Math.max(0, entry.timing?.durationMs ?? 0);
|
|
368
391
|
continue;
|
|
369
392
|
}
|
|
370
393
|
if (entry.kind !== "message") continue;
|
|
394
|
+
if (entry.timing?.durationMs !== undefined) llmTimedSteps++;
|
|
371
395
|
llmMs += Math.max(0, entry.timing?.durationMs ?? 0);
|
|
372
396
|
if (entry.timing?.ttftMs !== undefined) {
|
|
373
397
|
ttftMs += Math.max(0, entry.timing.ttftMs);
|
|
374
398
|
ttftSteps += 1;
|
|
375
399
|
}
|
|
376
|
-
if (entry.timing?.decodeMs
|
|
377
|
-
|
|
400
|
+
if ((entry.timing?.decodeMs ?? 0) > 0 && entry.tokens?.output !== undefined) {
|
|
401
|
+
decodeSteps++;
|
|
402
|
+
decodeMs += Math.max(0, entry.timing?.decodeMs ?? 0);
|
|
378
403
|
decodeTokens += Math.max(0, entry.tokens.output);
|
|
379
404
|
}
|
|
380
405
|
}
|
|
381
|
-
return { llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens };
|
|
406
|
+
return { llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens, llmTimedSteps, toolTimedCalls, decodeSteps };
|
|
382
407
|
}
|
|
383
408
|
|
|
384
409
|
function sessionStats(m: Managed): WebStats | null {
|
|
@@ -415,7 +440,7 @@ function coldSessionStats(m: Managed): WebStats {
|
|
|
415
440
|
cost += Number(usage.cost?.total ?? 0);
|
|
416
441
|
};
|
|
417
442
|
for (const entry of m.sm.getEntries() as any[]) {
|
|
418
|
-
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);
|
|
419
444
|
if (entry.type !== "message" || !entry.message) continue;
|
|
420
445
|
totalMessages += 1;
|
|
421
446
|
const message = entry.message;
|
|
@@ -497,7 +522,14 @@ function translate(m: Managed, evt: AgentSessionEvent): void {
|
|
|
497
522
|
// the browser receives the durable entry ID without reordering later tool events.
|
|
498
523
|
deferPublish(m, () => {
|
|
499
524
|
const msg = projectMessage(m, raw);
|
|
500
|
-
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
|
+
}
|
|
501
533
|
const usage = usageEvent(m);
|
|
502
534
|
return usage
|
|
503
535
|
? [{ type: "message", message: msg, phase: "end", ts: now }, usage]
|
|
@@ -597,8 +629,8 @@ function translate(m: Managed, evt: AgentSessionEvent): void {
|
|
|
597
629
|
else publish(m, { type: "model", thinkingLevel: (evt as any).level, ts: now });
|
|
598
630
|
break;
|
|
599
631
|
case "auto_retry_start":
|
|
600
|
-
//
|
|
601
|
-
// 不再走 error
|
|
632
|
+
// 重试是流程内通知:发一条结构化 retry 事件(界面聚合成「重试 n/max」一行),
|
|
633
|
+
// 不再走 error 通道,免得每次尝试都多出一条报错。
|
|
602
634
|
publish(m, {
|
|
603
635
|
type: "retry",
|
|
604
636
|
attempt: Number((evt as any).attempt) || 0,
|
|
@@ -607,6 +639,13 @@ function translate(m: Managed, evt: AgentSessionEvent): void {
|
|
|
607
639
|
ts: now,
|
|
608
640
|
});
|
|
609
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;
|
|
610
649
|
default:
|
|
611
650
|
break;
|
|
612
651
|
}
|
|
@@ -742,13 +781,9 @@ const SKILLS_SECTION_START = "The following skills provide specialized instructi
|
|
|
742
781
|
const SKILLS_SECTION_END = "</available_skills>";
|
|
743
782
|
|
|
744
783
|
/**
|
|
745
|
-
*
|
|
746
|
-
*
|
|
747
|
-
*
|
|
748
|
-
*
|
|
749
|
-
* 冷会话(只浏览历史、不冷启动 agent)拿不到组装后的 system prompt 和工具定义,这三项为 0、标 cold;
|
|
750
|
-
* Memory / Skills / Compacted / Buffer 不依赖 agent,照常从发现缓存、会话文件和设置里算,
|
|
751
|
-
* 不能因为没启动就整块归零(那样弹窗只剩消息几类)。
|
|
784
|
+
* 当前上下文的字符分类,优先读 SDK 投影中的系统分段、工具 schema 与摘要。
|
|
785
|
+
* 旧客户端仍可读取字符/4 的估算字段;新客户端仅将字符数作为分类数据。
|
|
786
|
+
* 没有持久化系统消息且尚未启动的会话,资源来自发现缓存并标记 cold。
|
|
752
787
|
*/
|
|
753
788
|
async function buildContextBreakdown(m: Managed, resources: ContextResource[]): Promise<ContextBreakdown> {
|
|
754
789
|
const session = m.session;
|
|
@@ -758,64 +793,56 @@ async function buildContextBreakdown(m: Managed, resources: ContextResource[]):
|
|
|
758
793
|
};
|
|
759
794
|
// Memory:AGENTS.md 等注入文件(contextResources 的 project 部分)
|
|
760
795
|
const memoryText = resources.filter((r) => r.source === "project").map((r) => r.content).join("\n\n");
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
796
|
+
// 字符分项只读取当前投影中的内容。历史压缩摘要、未激活工具不混入当前统计。
|
|
797
|
+
const characters = { systemPrompt: 0, systemTools: 0, customTools: 0, memory: 0, skills: 0, compacted: 0 };
|
|
798
|
+
try {
|
|
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;
|
|
776
813
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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;
|
|
789
826
|
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
} else {
|
|
794
|
-
// 冷会话:技能段用发现缓存里的清单按 SDK 同一个格式化函数估算(启动后改用实际 prompt 里的那段)
|
|
795
|
-
try {
|
|
827
|
+
} else {
|
|
828
|
+
// 初次启动前只能读到待注入的资源,界面保留 cold 提示。
|
|
829
|
+
characters.memory = memoryText.length;
|
|
796
830
|
const loader = peekResourceLoader(m.cwd) ?? getResourceLoader(m.cwd);
|
|
797
|
-
|
|
798
|
-
} catch {
|
|
799
|
-
/* 发现缓存还没好则跳过 */
|
|
831
|
+
characters.skills = formatSkillsForPrompt(loader.getSkills().skills).trim().length;
|
|
800
832
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
try {
|
|
804
|
-
const entries = m.sm.getEntries() as unknown as { type?: string; summary?: string }[];
|
|
805
|
-
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
806
|
-
const entry = entries[i];
|
|
807
|
-
if (entry?.type === "compaction" && typeof entry.summary === "string") {
|
|
808
|
-
breakdown.compacted = estimateTokensOf(entry.summary);
|
|
809
|
-
break;
|
|
810
|
-
}
|
|
833
|
+
for (const message of messages) {
|
|
834
|
+
if (message.role === "compactionSummary" || message.role === "branchSummary") characters.compacted += message.summary?.length ?? 0;
|
|
811
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);
|
|
812
839
|
} catch {
|
|
813
|
-
/*
|
|
840
|
+
/* 投影不可用时不给字符分项,不能从旧 token 估算反推字符数。 */
|
|
814
841
|
}
|
|
815
|
-
// auto-compact
|
|
842
|
+
// auto-compact 预留读本工作区、当前模型实际使用的设置。
|
|
816
843
|
try {
|
|
817
|
-
const
|
|
818
|
-
if (
|
|
844
|
+
const compaction = getSettingsManager(m.cwd).getCompactionSettings(session?.model);
|
|
845
|
+
if (compaction.enabled) breakdown.autoCompactBuffer = compaction.reserveTokens;
|
|
819
846
|
} catch {
|
|
820
847
|
/* 设置读取失败则保持 0 */
|
|
821
848
|
}
|
|
@@ -1208,11 +1235,7 @@ async function reloadManagedResources(m: Managed): Promise<boolean> {
|
|
|
1208
1235
|
m.resourceReloadPending = false;
|
|
1209
1236
|
try {
|
|
1210
1237
|
await m.session.reload();
|
|
1211
|
-
|
|
1212
|
-
// 会话手里还是旧对象。按 provider/id 从(已就地刷新的)共享 runtime 取一次新的。
|
|
1213
|
-
const current = m.session.model;
|
|
1214
|
-
const fresh = current && m.session.modelRuntime.getModel(current.provider, current.id);
|
|
1215
|
-
if (fresh && fresh !== current) m.session.agent.state.model = fresh;
|
|
1238
|
+
swapCurrentModelObject(m.session);
|
|
1216
1239
|
return true;
|
|
1217
1240
|
} catch (err) {
|
|
1218
1241
|
publish(m, { type: "error", message: `重载扩展失败:${String((err as Error)?.message ?? err)}`, ts: Date.now() });
|
|
@@ -1222,6 +1245,32 @@ async function reloadManagedResources(m: Managed): Promise<boolean> {
|
|
|
1222
1245
|
}
|
|
1223
1246
|
}
|
|
1224
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
|
+
|
|
1225
1274
|
export async function reloadSessionsForCwd(cwd?: string): Promise<number> {
|
|
1226
1275
|
const key = (p: string) => (process.platform === "win32" ? p.replace(/\\/g, "/").toLowerCase() : p.replace(/\\/g, "/"));
|
|
1227
1276
|
let n = 0;
|
|
@@ -1325,42 +1374,13 @@ export async function buildSnapshot(m: Managed): Promise<WebSnapshot> {
|
|
|
1325
1374
|
const usage = session.getContextUsage() ?? null;
|
|
1326
1375
|
contextUsage = usage ? { tokens: usage.tokens, contextWindow: usage.contextWindow, percent: usage.percent } : null;
|
|
1327
1376
|
} else if (model) {
|
|
1328
|
-
//
|
|
1329
|
-
// 上下文规模)除以模型窗口估算,避免上下文计量永远显示 0% / —
|
|
1377
|
+
// 冷会话也使用当前分支投影,包含最后一次 API 用量之后的用户消息和工具输出。
|
|
1330
1378
|
try {
|
|
1331
1379
|
const rt = await getModelRuntime();
|
|
1332
1380
|
const modelMeta = (rt.getModels(model.provider) as any[]).find((x) => String(x.id) === model.id);
|
|
1333
|
-
|
|
1334
|
-
let lastTurnTokens = 0;
|
|
1335
|
-
// 与 SDK getContextUsage 同一条规则:最近一次压缩之后还没有新的助手回复时,上下文大小未知(不能拿压缩前的用量充数)
|
|
1336
|
-
const entries = m.sm.getEntries() as Array<{ type?: string; message?: { role?: string; stopReason?: string } }>;
|
|
1337
|
-
let compactedSinceLastAssistant = false;
|
|
1338
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
1339
|
-
const e = entries[i];
|
|
1340
|
-
if (e.type === "compaction" || e.type === "branch_summary") {
|
|
1341
|
-
compactedSinceLastAssistant = true;
|
|
1342
|
-
break;
|
|
1343
|
-
}
|
|
1344
|
-
if (e.type === "message" && e.message?.role === "assistant" && e.message.stopReason !== "error" && e.message.stopReason !== "aborted") break;
|
|
1345
|
-
}
|
|
1346
|
-
for (let i = messages.length - 1; i >= 0 && !compactedSinceLastAssistant; i--) {
|
|
1347
|
-
const msg = messages[i];
|
|
1348
|
-
if (msg.role === "assistant" && msg.usage) {
|
|
1349
|
-
lastTurnTokens =
|
|
1350
|
-
Number(msg.usage.input ?? 0) +
|
|
1351
|
-
Number(msg.usage.cacheRead ?? 0) +
|
|
1352
|
-
Number(msg.usage.cacheWrite ?? 0) +
|
|
1353
|
-
Number(msg.usage.output ?? 0);
|
|
1354
|
-
break;
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
if (window > 0 && lastTurnTokens > 0) {
|
|
1358
|
-
contextUsage = { tokens: lastTurnTokens, contextWindow: window, percent: (lastTurnTokens / window) * 100 };
|
|
1359
|
-
} else if (window > 0) {
|
|
1360
|
-
contextUsage = { tokens: null, contextWindow: window, percent: null };
|
|
1361
|
-
}
|
|
1381
|
+
contextUsage = projectedContextUsage(m.sm.buildSessionProjection(), m.sm.getBranch(), Number(modelMeta?.contextWindow ?? 0));
|
|
1362
1382
|
} catch {
|
|
1363
|
-
/*
|
|
1383
|
+
/* 模型或投影不可用时用量未知。 */
|
|
1364
1384
|
}
|
|
1365
1385
|
}
|
|
1366
1386
|
|
|
@@ -1600,6 +1620,8 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1600
1620
|
if (!model) return { ok: false, error: `unknown model ${cmd.provider}/${cmd.modelId}` };
|
|
1601
1621
|
await session.setModel(model, { persist: false });
|
|
1602
1622
|
reapplyDesiredLevel(m, session);
|
|
1623
|
+
// 新会话页选的模型(fresh):思考档位按新会话的规则定;之后显式选的档位仍会覆盖
|
|
1624
|
+
if (cmd.fresh === true && !m.desiredThinkingLevel) applyNewSessionThinking(m, session);
|
|
1603
1625
|
publishModelState(m, session);
|
|
1604
1626
|
return { ok: true, data: { thinkingLevel: session.thinkingLevel, thinkingLevels: session.getAvailableThinkingLevels() } };
|
|
1605
1627
|
}
|
|
@@ -1712,8 +1734,17 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
|
|
|
1712
1734
|
}
|
|
1713
1735
|
case "reload": {
|
|
1714
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
|
+
}
|
|
1715
1745
|
const session = await ensureSession(m);
|
|
1716
1746
|
await session.reload();
|
|
1747
|
+
swapCurrentModelObject(session);
|
|
1717
1748
|
return { ok: true };
|
|
1718
1749
|
}
|
|
1719
1750
|
case "fork": {
|
package/src/lib/files-service.ts
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
* files-service:会话工作区的文件浏览(详情栏「文件」面板)。
|
|
3
3
|
* 安全边界:目标路径必须落在工作区根内(realpath 双重校验,防符号链接逃逸)。
|
|
4
4
|
*/
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
5
|
+
import { execFile, spawn } from "node:child_process";
|
|
6
6
|
import fs from "node:fs/promises";
|
|
7
7
|
import path from "node:path";
|
|
8
|
+
import { promisify } from "node:util";
|
|
8
9
|
import { BoundaryError, isPathInside, resolveWorkspacePath } from "./path-security";
|
|
9
10
|
import { getAgentDir } from "./pi";
|
|
11
|
+
import { resolveGitBin } from "./growth-service";
|
|
10
12
|
import { utf8Prefix } from "./text-sanitize";
|
|
11
13
|
|
|
14
|
+
const exec = promisify(execFile);
|
|
15
|
+
|
|
12
16
|
export interface FileEntry {
|
|
13
17
|
name: string;
|
|
14
18
|
kind: "dir" | "file";
|
|
@@ -29,6 +33,101 @@ const MAX_READ_BYTES = 256 * 1024;
|
|
|
29
33
|
/** 单次目录响应上限;后续条目通过 offset 分页获取。 */
|
|
30
34
|
const MAX_ENTRIES = 2000;
|
|
31
35
|
|
|
36
|
+
/**
|
|
37
|
+
* @ 引用的候选索引上限。一次拉给浏览器做本地模糊匹配,所以是一份完整清单:
|
|
38
|
+
* 每敲一个字符不再发请求(实测本仓库 700 余条 ≈ 30 KB)。
|
|
39
|
+
* 超大仓库截断到上限并在界面标出,不静默少候选。
|
|
40
|
+
*/
|
|
41
|
+
const INDEX_LIMIT = 20_000;
|
|
42
|
+
/** 非 git 工作区的退路:只跳过这些明显不该当候选的重目录,其余照常列举 */
|
|
43
|
+
const INDEX_SKIP_DIRS = new Set([".git", "node_modules", ".next", ".next-dev", ".venv", "venv", "__pycache__", "dist", "build", "out", "target", ".turbo", ".cache"]);
|
|
44
|
+
const INDEX_MAX_DEPTH = 12;
|
|
45
|
+
|
|
46
|
+
export interface WorkspaceIndex {
|
|
47
|
+
/** 工作区根下的相对路径("/" 分隔)。只给文件:文件夹由前端从文件路径推导 */
|
|
48
|
+
files: string[];
|
|
49
|
+
/** git = `git ls-files`(尊重 .gitignore);disk = 不是仓库时的目录遍历退路 */
|
|
50
|
+
source: "git" | "disk";
|
|
51
|
+
/** 候选被上限截断:匹配结果可能不全,界面要说明 */
|
|
52
|
+
truncated: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** `git ls-files -z` 输出 → 相对路径数组(去重) */
|
|
56
|
+
export function parseLsFiles(raw: string): string[] {
|
|
57
|
+
const seen = new Set<string>();
|
|
58
|
+
for (const entry of raw.split("\0")) {
|
|
59
|
+
if (!entry) continue;
|
|
60
|
+
const rel = entry.replace(/\\/g, "/");
|
|
61
|
+
if (rel.startsWith("/") || rel.split("/").includes("..")) continue;
|
|
62
|
+
seen.add(rel);
|
|
63
|
+
}
|
|
64
|
+
return [...seen];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 不是仓库(或机器上没有 git)时的退路:广度优先遍历,只收文件 */
|
|
68
|
+
async function walkWorkspaceFiles(root: string): Promise<{ files: string[]; truncated: boolean }> {
|
|
69
|
+
const files: string[] = [];
|
|
70
|
+
let queue: Array<{ rel: string; depth: number }> = [{ rel: "", depth: 0 }];
|
|
71
|
+
while (queue.length) {
|
|
72
|
+
const next: typeof queue = [];
|
|
73
|
+
for (const dir of queue) {
|
|
74
|
+
const entries = await fs.readdir(path.join(root, dir.rel), { withFileTypes: true }).catch(() => []);
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const rel = dir.rel ? `${dir.rel}/${entry.name}` : entry.name;
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
if (dir.depth + 1 <= INDEX_MAX_DEPTH && !INDEX_SKIP_DIRS.has(entry.name)) next.push({ rel, depth: dir.depth + 1 });
|
|
79
|
+
} else if (entry.isFile()) {
|
|
80
|
+
files.push(rel);
|
|
81
|
+
if (files.length > INDEX_LIMIT) return { files: files.slice(0, INDEX_LIMIT), truncated: true };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
queue = next;
|
|
86
|
+
}
|
|
87
|
+
return { files, truncated: false };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* git 失败时只有「不是仓库」「没装 git」两种该退回遍历——界面会把 disk 来源解释成这两种情况。
|
|
92
|
+
* 其它失败(safe.directory 拒绝、索引损坏、超时)照实报错:退回遍历会把 .gitignore 掉的文件混进候选,还给出错误的解释。
|
|
93
|
+
*/
|
|
94
|
+
function gitIndexFailure(error: unknown): "fallback" | Error {
|
|
95
|
+
const e = error as NodeJS.ErrnoException & { stderr?: string; killed?: boolean };
|
|
96
|
+
if (e.code === "ENOENT") return "fallback";
|
|
97
|
+
const stderr = String(e.stderr ?? "");
|
|
98
|
+
if (/not a git repository/i.test(stderr)) return "fallback";
|
|
99
|
+
if (e.killed) return new Error("git ls-files timed out");
|
|
100
|
+
const fatal = stderr.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
101
|
+
return new Error(`git ls-files failed: ${fatal.find((line) => line.startsWith("fatal:")) ?? fatal[0] ?? e.message}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @ 引用的候选清单:git 可见的文件(含未被忽略的未跟踪文件)。
|
|
106
|
+
* `git ls-files` 的路径相对当前目录,所以 cwd 就是工作区根,输出天然是工作区相对路径。
|
|
107
|
+
*/
|
|
108
|
+
export async function listWorkspaceIndex(cwdValue: unknown): Promise<WorkspaceIndex> {
|
|
109
|
+
const root = await resolveWorkspacePath(cwdValue);
|
|
110
|
+
const git = await resolveGitBin();
|
|
111
|
+
try {
|
|
112
|
+
const { stdout } = await exec(git, ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
113
|
+
cwd: root,
|
|
114
|
+
timeout: 15_000,
|
|
115
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
116
|
+
windowsHide: true,
|
|
117
|
+
// LC_ALL=C:报错固定为英文,「不是仓库」才认得出来(中文界面的 git 会翻译这句);-z 输出的路径不受影响
|
|
118
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: "0", GIT_TERMINAL_PROMPT: "0", LC_ALL: "C" },
|
|
119
|
+
});
|
|
120
|
+
const files = parseLsFiles(stdout).sort((a, b) => a.localeCompare(b));
|
|
121
|
+
return { files: files.slice(0, INDEX_LIMIT), source: "git", truncated: files.length > INDEX_LIMIT };
|
|
122
|
+
} catch (error) {
|
|
123
|
+
const failure = gitIndexFailure(error);
|
|
124
|
+
if (failure !== "fallback") throw failure;
|
|
125
|
+
const walked = await walkWorkspaceFiles(root);
|
|
126
|
+
return { files: walked.files.sort((a, b) => a.localeCompare(b)), source: "disk", truncated: walked.truncated };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
32
131
|
function toRel(root: string, target: string): string {
|
|
33
132
|
const rel = path.relative(root, target).replace(/\\/g, "/");
|
|
34
133
|
return rel === "" ? "." : rel;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 全站数字显示格式。
|
|
3
|
+
*
|
|
4
|
+
* 精度口径(用户要求「精确到小数点后两位」):
|
|
5
|
+
* - 比率(缓存命中 / 上下文占用)、速率(tok/s)、秒级时长:一律两位小数;
|
|
6
|
+
* - token 量:≥1000 用 K、≥100 万用 M,都两位小数;<1000 原样整数(计数没有小数含义);
|
|
7
|
+
* - 轮次 / 步数 / 文件数等计数:整数;
|
|
8
|
+
* - 分钟级时长保持 `19m30s`(`19m30.42s` 更难读)、金额保持四位(`$0.0123` 是惯例)。
|
|
9
|
+
*
|
|
10
|
+
* 以前 ChatWindow / ContextMeter / TrajectoryView 各有一份 fmtTok,同一个数字在三处
|
|
11
|
+
* 显示不同(262400 → `262K` / `262.4K` / `262.4K`;53000000 → `53M` / `53000.0K` / `53.0M`),
|
|
12
|
+
* 统一到这里。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** token 量:K / M 两位小数,<1000 整数 */
|
|
16
|
+
export function fmtTokens(value: number): string {
|
|
17
|
+
if (!Number.isFinite(value)) return "—";
|
|
18
|
+
if (Math.abs(value) < 1_000) return String(Math.round(value));
|
|
19
|
+
// 先按 K 缩放;四舍五入到两位小数后已到 1000.00K 就升级成 M,避免出现「1000.00K」
|
|
20
|
+
const k = value / 1_000;
|
|
21
|
+
if (Math.abs(Number(k.toFixed(2))) < 1_000) return `${k.toFixed(2)}K`;
|
|
22
|
+
return `${(value / 1_000_000).toFixed(2)}M`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 计数(轮次 / 步数 / 文件数):整数 */
|
|
26
|
+
export function fmtCount(value: number): string {
|
|
27
|
+
return Number.isFinite(value) ? String(Math.round(value)) : "—";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 百分比:入参是 0–100 的数值,输出两位小数 */
|
|
31
|
+
export function fmtPercent(percent: number): string {
|
|
32
|
+
return `${(Number.isFinite(percent) ? percent : 0).toFixed(2)}%`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 速率:tok/s,两位小数 */
|
|
36
|
+
export function fmtThroughput(tokensPerSecond: number): string {
|
|
37
|
+
return (Number.isFinite(tokensPerSecond) ? tokensPerSecond : 0).toFixed(2);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 时长:不足 60 秒两位小数(`5.32s`),到分钟级用 `19m30s`(秒级再补小数只会更难读)。
|
|
42
|
+
*/
|
|
43
|
+
export function fmtDuration(ms: number): string {
|
|
44
|
+
const seconds = Math.max(0, Number.isFinite(ms) ? ms : 0) / 1_000;
|
|
45
|
+
if (seconds < 60) return `${seconds.toFixed(2)}s`;
|
|
46
|
+
const whole = Math.round(seconds);
|
|
47
|
+
return `${Math.floor(whole / 60)}m${whole % 60}s`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 文件大小:B 整数,KB / MB 两位小数 */
|
|
51
|
+
export function fmtBytes(bytes: number): string {
|
|
52
|
+
if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
|
|
53
|
+
if (bytes < 1_024) return `${Math.round(bytes)} B`;
|
|
54
|
+
if (bytes < 1_024 * 1_024) return `${(bytes / 1_024).toFixed(2)} KB`;
|
|
55
|
+
return `${(bytes / 1_024 / 1_024).toFixed(2)} MB`;
|
|
56
|
+
}
|