@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
|
@@ -1,48 +1,23 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* 上下文占用:
|
|
5
|
-
* 圆环常驻按钮 + 弹窗显示「百分比 / ~已用 / 窗口」与 13 类分段占用条
|
|
6
|
-
* (mac 磁盘存储条式的多色分段,含 Free Space 段)。
|
|
7
|
-
* 分段口径:全部为字符/4 启发式估算(与 SDK estimateTokens 一致),
|
|
8
|
-
* 服务端分项(System Prompt / Tools / Memory / Skills / Compacted / Buffer)来自快照,
|
|
9
|
-
* 消息分项(User / Agent Text / Thinking / Tool Call / Tool Output)弹窗打开时前端统计。
|
|
10
|
-
* 冷会话(只浏览历史、agent 没启动)拿不到组装后的 System Prompt 和工具定义:
|
|
11
|
-
* 其余分项照常显示,另给「现在统计」按需启动会话,分项随后经 context_breakdown 事件刷新。
|
|
12
|
-
* 无对话时不渲染(由父组件经 contextVisible 控制)。
|
|
13
|
-
*/
|
|
3
|
+
/** 上下文总量是 SDK 估算;API 输入用量与内容字符分类分别展示,不能按比例分摊 token。 */
|
|
14
4
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
15
5
|
import { useI18n } from "@/i18n";
|
|
6
|
+
import { fmtPercent, fmtTokens } from "@/lib/format";
|
|
7
|
+
import { formatCapacity, parseCapacity } from "@/lib/model-draft";
|
|
16
8
|
import { classifyMessageChars } from "@/lib/process-format";
|
|
17
|
-
import type { ContextBreakdown } from "@/lib/types";
|
|
18
|
-
|
|
19
|
-
function fmtTok(n: number): string {
|
|
20
|
-
return n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(Math.round(n));
|
|
21
|
-
}
|
|
9
|
+
import type { ContextBreakdown, TrajTokens } from "@/lib/types";
|
|
22
10
|
|
|
23
|
-
|
|
24
|
-
interface Segment {
|
|
25
|
-
key: string;
|
|
26
|
-
label: string;
|
|
27
|
-
tokens: number;
|
|
28
|
-
color: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function ContextMeter({
|
|
32
|
-
percent,
|
|
33
|
-
tokens,
|
|
34
|
-
contextWindow,
|
|
35
|
-
source,
|
|
36
|
-
}: {
|
|
11
|
+
export function ContextMeter({ percent, tokens, contextWindow, model, source }: {
|
|
37
12
|
percent: number | null;
|
|
38
13
|
tokens: number | null;
|
|
39
14
|
contextWindow: number | null;
|
|
40
|
-
/**
|
|
15
|
+
/** 当前会话的模型:有它才能在弹窗里改上下文窗口 */
|
|
16
|
+
model?: { provider: string; id: string } | null;
|
|
41
17
|
source?: {
|
|
42
18
|
systemChars: number;
|
|
43
|
-
messages: { role?: string; content: Array<{ type: string; text?: string; thinking?: string; arguments?: unknown }
|
|
19
|
+
messages: Array<{ role?: string; content: Array<{ type: string; text?: string; thinking?: string; arguments?: unknown; name?: string }>; usage?: TrajTokens; stopReason?: string }>;
|
|
44
20
|
breakdown?: ContextBreakdown;
|
|
45
|
-
/** 冷会话里点「现在统计」:启动会话(prepare),System Prompt 与工具定义才算得出来 */
|
|
46
21
|
loadFull?: () => Promise<unknown> | unknown;
|
|
47
22
|
};
|
|
48
23
|
}) {
|
|
@@ -52,154 +27,229 @@ export function ContextMeter({
|
|
|
52
27
|
const { t } = useI18n();
|
|
53
28
|
const cold = Boolean(source?.breakdown?.cold);
|
|
54
29
|
|
|
55
|
-
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
if (!cold) setCounting(false);
|
|
58
|
-
}, [cold]);
|
|
59
|
-
|
|
30
|
+
useEffect(() => { if (!cold) setCounting(false); }, [cold]);
|
|
60
31
|
useEffect(() => {
|
|
61
32
|
if (!open) return;
|
|
62
|
-
const h = (e: MouseEvent) => {
|
|
63
|
-
if (!ref.current?.contains(e.target as Node)) setOpen(false);
|
|
64
|
-
};
|
|
33
|
+
const h = (e: MouseEvent) => { if (!ref.current?.contains(e.target as Node)) setOpen(false); };
|
|
65
34
|
document.addEventListener("mousedown", h);
|
|
66
35
|
return () => document.removeEventListener("mousedown", h);
|
|
67
36
|
}, [open]);
|
|
68
37
|
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
const
|
|
38
|
+
// 流式期间只在打开弹窗后扫描,分类是实际文本字符数,不是假设的 token 数。
|
|
39
|
+
const chars = useMemo(() => classifyMessageChars(open ? source?.messages ?? [] : []), [open, source]);
|
|
40
|
+
const lastUsage = useMemo(() => open ? source?.messages.findLast((m) => m.role === "assistant" && m.stopReason !== "error" && m.stopReason !== "aborted" && m.stopReason !== "pending" &&
|
|
41
|
+
(m.usage?.input ?? 0) + (m.usage?.cacheRead ?? 0) + (m.usage?.cacheWrite ?? 0) + (m.usage?.output ?? 0) > 0)?.usage : undefined, [open, source]);
|
|
42
|
+
const measuredInput = lastUsage ? (lastUsage.input ?? 0) + (lastUsage.cacheRead ?? 0) + (lastUsage.cacheWrite ?? 0) : null;
|
|
72
43
|
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
agentToolCall: Math.max(0, Math.round(chars.agentToolCall / 4)),
|
|
82
|
-
toolOutput: Math.max(0, Math.round(chars.toolOutput / 4)),
|
|
83
|
-
};
|
|
84
|
-
}, [open, source]);
|
|
44
|
+
// 刚改过窗口:服务端已落盘,但页面上的 contextWindow 还是改之前的值(冷会话要等下一次快照)。
|
|
45
|
+
// 在它变之前先显示刚保存的值;contextWindow 一变(快照 / 用量追上来,或别处改了)就以 props 为准。
|
|
46
|
+
const modelKey = model ? `${model.provider}/${model.id}` : "";
|
|
47
|
+
const [applied, setApplied] = useState<{ key: string; from: number; value: number } | null>(null);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (applied && (applied.key !== modelKey || contextWindow !== applied.from)) setApplied(null);
|
|
50
|
+
}, [applied, modelKey, contextWindow]);
|
|
51
|
+
const override = applied && applied.key === modelKey && contextWindow === applied.from ? applied.value : null;
|
|
85
52
|
|
|
86
|
-
const
|
|
87
|
-
const
|
|
53
|
+
const known = tokens !== null && Number.isFinite(tokens) && tokens >= 0;
|
|
54
|
+
const used = known ? tokens : 0;
|
|
55
|
+
const window_ = override ?? contextWindow ?? 0;
|
|
56
|
+
const recomputed = override !== null && known;
|
|
57
|
+
const p = recomputed ? used / override * 100 : percent ?? 0;
|
|
58
|
+
const usageLabel = percent === null && !recomputed ? t.contextUnknown : `${fmtPercent(p)} ${t.contextEstimated}`;
|
|
88
59
|
const r = 5.5;
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
[
|
|
109
|
-
{ key: "system", label: t.contextSystem, tokens: Math.max(0, Math.round((source?.systemChars ?? 0) / 4)), color: "var(--dsw-accent)" },
|
|
110
|
-
{ key: "messages", label: t.contextMessages, tokens: messageSegments.user + messageSegments.agentText + messageSegments.agentThinking, color: "var(--dsw-success)" },
|
|
111
|
-
{ key: "tools", label: t.contextTools, tokens: Math.max(0, used - Math.max(0, Math.round((source?.systemChars ?? 0) / 4)) - messageSegments.user - messageSegments.agentText - messageSegments.agentThinking), color: "var(--dsw-warn)" },
|
|
112
|
-
];
|
|
113
|
-
|
|
114
|
-
// Free Space:窗口余量(窗口未知则不显示);分段条不归一化到已用之和
|
|
115
|
-
const usedSum = segments.reduce((sum, s) => sum + s.tokens, 0);
|
|
116
|
-
const free = window_ > 0 ? Math.max(0, window_ - usedSum) : 0;
|
|
117
|
-
const barTotal = window_ > 0 ? window_ : usedSum || 1;
|
|
118
|
-
const bar = (n: number) => `${(n / barTotal) * 100}%`;
|
|
60
|
+
const circumference = 2 * Math.PI * r;
|
|
61
|
+
const reserved = Math.max(0, source?.breakdown?.autoCompactBuffer ?? 0);
|
|
62
|
+
// 超过窗口时预留不再画到窗口外;未知用量不能将整扇窗口显示为 Free Space。
|
|
63
|
+
const visibleReserved = known && window_ > 0 ? Math.min(reserved, Math.max(0, window_ - used)) : 0;
|
|
64
|
+
const free = known && window_ > 0 ? Math.max(0, window_ - used - reserved) : null;
|
|
65
|
+
const b = source?.breakdown?.characters;
|
|
66
|
+
const rows = [
|
|
67
|
+
{ key: "system", label: t.contextSystemPrompt, value: b?.systemPrompt, color: "var(--dsw-accent)" },
|
|
68
|
+
{ key: "builtin", label: t.contextSystemTools, value: b?.systemTools, color: "#8B7EC8" },
|
|
69
|
+
{ key: "custom", label: t.contextCustomTools, value: b?.customTools, color: "#B39DDB" },
|
|
70
|
+
{ key: "memory", label: t.contextMemory, value: b?.memory, color: "#5FA8D3" },
|
|
71
|
+
{ key: "skills", label: t.contextSkills, value: b?.skills, color: "#4FB286" },
|
|
72
|
+
{ key: "user", label: t.contextUserMessages, value: chars.user, color: "var(--dsw-success)" },
|
|
73
|
+
{ key: "text", label: t.contextAgentText, value: chars.agentText, color: "#7BC96F" },
|
|
74
|
+
{ key: "thinking", label: t.contextAgentThinking, value: chars.agentThinking, color: "#C9A66B" },
|
|
75
|
+
{ key: "call", label: t.contextAgentToolCall, value: chars.agentToolCall, color: "#D3A15F" },
|
|
76
|
+
{ key: "output", label: t.contextToolOutput, value: chars.toolOutput, color: "var(--dsw-warn)" },
|
|
77
|
+
{ key: "compacted", label: t.contextCompacted, value: b?.compacted, color: "#A05A78" },
|
|
78
|
+
].filter((row) => row.value !== undefined && row.value > 0);
|
|
119
79
|
|
|
120
80
|
return (
|
|
121
81
|
<div ref={ref} className="relative">
|
|
122
|
-
<button
|
|
123
|
-
className="icon-btn"
|
|
124
|
-
style={{ width: 28, height: 28 }}
|
|
125
|
-
title={usageLabel}
|
|
126
|
-
onClick={() => setOpen((o) => !o)}
|
|
127
|
-
>
|
|
82
|
+
<button className="icon-btn" style={{ width: 28, height: 28 }} title={usageLabel} aria-label={usageLabel} onClick={() => setOpen((o) => !o)}>
|
|
128
83
|
<svg width="14" height="14" viewBox="0 0 14 14">
|
|
129
84
|
<circle cx="7" cy="7" r={r} fill="none" stroke="var(--dsw-border-l3)" strokeWidth="2" />
|
|
130
|
-
<circle
|
|
131
|
-
|
|
132
|
-
fill="none"
|
|
133
|
-
stroke={p != null && p > 85 ? "var(--dsw-warn)" : "var(--dsw-accent)"}
|
|
134
|
-
strokeWidth="2"
|
|
135
|
-
strokeDasharray={`${(c * shown) / 100} ${c}`}
|
|
136
|
-
transform="rotate(-90 7 7)"
|
|
137
|
-
strokeLinecap="round"
|
|
138
|
-
/>
|
|
85
|
+
<circle cx="7" cy="7" r={r} fill="none" stroke={p > 85 ? "var(--dsw-warn)" : "var(--dsw-accent)"} strokeWidth="2"
|
|
86
|
+
strokeDasharray={`${circumference * Math.max(0, Math.min(100, p)) / 100} ${circumference}`} transform="rotate(-90 7 7)" strokeLinecap="round" />
|
|
139
87
|
</svg>
|
|
140
88
|
</button>
|
|
141
89
|
{open && (
|
|
142
|
-
<div
|
|
143
|
-
className="
|
|
144
|
-
style={{
|
|
145
|
-
|
|
146
|
-
<div className="font-medium" style={{ color: "var(--dsw-label-primary)" }}>
|
|
147
|
-
{usageLabel}
|
|
148
|
-
</div>
|
|
149
|
-
<div style={{ color: "var(--dsw-label-tertiary)" }} className="mt-1">
|
|
150
|
-
{tokens === null ? "—" : `~${fmtTok(used)}`} / {window_ > 0 ? fmtTok(window_) : "—"}
|
|
90
|
+
<div className="popover absolute bottom-9 right-0 z-50 w-72 p-4" style={{ fontSize: "var(--dsh-content-font-size-secondary)" }}>
|
|
91
|
+
<div className="font-medium" style={{ color: "var(--dsw-label-primary)" }}>{usageLabel}</div>
|
|
92
|
+
<div className="mt-1" style={{ color: "var(--dsw-label-tertiary)" }}>
|
|
93
|
+
{known ? `~${fmtTokens(used)}` : "—"} / {window_ > 0 ? fmtTokens(window_) : "—"} tok
|
|
151
94
|
</div>
|
|
152
|
-
|
|
153
|
-
{
|
|
154
|
-
{usedSum > 0 && (
|
|
95
|
+
{measuredInput !== null && <div className="mt-2 flex items-center justify-between gap-2"><span>{t.contextApiInput}</span><span title={`${measuredInput} tok`}>{fmtTokens(measuredInput)} tok</span></div>}
|
|
96
|
+
{known && window_ > 0 && (
|
|
155
97
|
<div className="mt-3 flex h-2 w-full overflow-hidden rounded-full" style={{ background: "var(--dsw-selector)" }} aria-hidden>
|
|
156
|
-
{
|
|
157
|
-
|
|
158
|
-
))}
|
|
159
|
-
{free > 0 && <span style={{ width: bar(free), background: "transparent" }} />}
|
|
98
|
+
<span className="flex-none" style={{ width: `${Math.min(100, used / window_ * 100)}%`, background: "var(--dsw-accent)" }} />
|
|
99
|
+
{visibleReserved > 0 && <span className="flex-none" style={{ width: `${visibleReserved / window_ * 100}%`, background: "var(--dsw-border-l3)" }} />}
|
|
160
100
|
</div>
|
|
161
101
|
)}
|
|
162
102
|
<div className="mt-2 flex flex-col gap-1" style={{ color: "var(--dsw-label-tertiary)" }}>
|
|
163
|
-
{
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
103
|
+
{reserved > 0 && <DataRow label={t.contextAutoCompactBuffer} value={`${fmtTokens(reserved)} tok`} exact={`${reserved} tok`} color="var(--dsw-border-l3)" />}
|
|
104
|
+
{window_ > 0 && <DataRow label={t.contextFreeSpace} value={free === null ? "—" : `~${fmtTokens(free)} tok`} color="var(--dsw-selector)" />}
|
|
105
|
+
</div>
|
|
106
|
+
<div className="mt-3 font-medium">{t.contextCharacterBreakdown}</div>
|
|
107
|
+
<div className="mt-1" style={{ fontSize: 11.5, color: "var(--dsw-label-tertiary)" }}>{t.contextCharacterHint}</div>
|
|
108
|
+
<div className="mt-2 flex flex-col gap-1" style={{ color: "var(--dsw-label-tertiary)" }}>
|
|
109
|
+
{rows.map((row) => <DataRow key={row.key} label={row.label} value={`${fmtTokens(row.value!)} ${t.contextChars}`} exact={`${row.value} ${t.contextChars}`} color={row.color} />)}
|
|
167
110
|
</div>
|
|
168
|
-
{cold && source?.loadFull
|
|
111
|
+
{cold && source?.loadFull && (
|
|
169
112
|
<div className="mt-3 flex items-center justify-between gap-2" style={{ color: "var(--dsw-label-tertiary)" }}>
|
|
170
113
|
<span className="min-w-0" style={{ fontSize: 11.5 }}>{t.contextColdHint}</span>
|
|
171
|
-
<button
|
|
172
|
-
type="button"
|
|
173
|
-
className="btn-outline flex-none"
|
|
174
|
-
style={{ height: 24, padding: "0 10px", fontSize: 12 }}
|
|
175
|
-
disabled={counting}
|
|
114
|
+
<button type="button" className="btn-outline flex-none" style={{ height: 24, padding: "0 10px", fontSize: 12 }} disabled={counting}
|
|
176
115
|
onClick={() => {
|
|
177
116
|
setCounting(true);
|
|
178
|
-
// 启动失败(例如没配模型)就把按钮还回来;成功时等 context_breakdown 事件把冷标记去掉
|
|
179
117
|
void Promise.resolve(source.loadFull?.()).then((result) => {
|
|
180
118
|
if ((result as { success?: boolean } | undefined)?.success === false) setCounting(false);
|
|
181
119
|
}, () => setCounting(false));
|
|
182
|
-
}}
|
|
183
|
-
>
|
|
184
|
-
{counting ? t.contextCounting : t.contextCountNow}
|
|
185
|
-
</button>
|
|
120
|
+
}}>{counting ? t.contextCounting : t.contextCountNow}</button>
|
|
186
121
|
</div>
|
|
187
|
-
)
|
|
122
|
+
)}
|
|
123
|
+
{model && window_ > 0 && (
|
|
124
|
+
<WindowEditor key={modelKey} model={model} value={window_} onSaved={(value) => setApplied({ key: modelKey, from: contextWindow ?? 0, value })} />
|
|
125
|
+
)}
|
|
188
126
|
</div>
|
|
189
127
|
)}
|
|
190
128
|
</div>
|
|
191
129
|
);
|
|
192
130
|
}
|
|
193
131
|
|
|
194
|
-
function
|
|
195
|
-
|
|
132
|
+
async function postModels<T>(body: Record<string, unknown>): Promise<T> {
|
|
133
|
+
const response = await fetch("/api/models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
|
134
|
+
const payload = (await response.json().catch(() => null)) as { success?: boolean; data?: T; error?: string } | null;
|
|
135
|
+
if (!response.ok || !payload?.success) throw new Error(payload?.error || `request failed (${response.status})`);
|
|
136
|
+
return payload.data as T;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 弹窗底部的上下文窗口:平时显示精确值,「修改」后填数字保存。
|
|
141
|
+
* 不用滑块:模型的真实上限是 128000、200000 这类精确值,拖出来的近似值多一点上游就 400。
|
|
142
|
+
* 内置目录有原值就并排给出、填大了提醒;改的是该模型的全局配置,这一点写在输入框下面。
|
|
143
|
+
*/
|
|
144
|
+
function WindowEditor({ model, value, onSaved }: { model: { provider: string; id: string }; value: number; onSaved: (value: number) => void }) {
|
|
145
|
+
const { t } = useI18n();
|
|
146
|
+
const [editing, setEditing] = useState(false);
|
|
147
|
+
const [text, setText] = useState("");
|
|
148
|
+
/** undefined = 还在查(不显示上限提示,免得先闪一句「没有内置值」) */
|
|
149
|
+
const [builtin, setBuiltin] = useState<number | null | undefined>(undefined);
|
|
150
|
+
const [saving, setSaving] = useState(false);
|
|
151
|
+
const [error, setError] = useState<string | null>(null);
|
|
152
|
+
const infoSeq = useRef(0);
|
|
153
|
+
const parsed = parseCapacity(text);
|
|
154
|
+
const target = parsed !== undefined && !Number.isNaN(parsed) ? parsed : null;
|
|
155
|
+
const valid = target !== null;
|
|
156
|
+
const over = target !== null && typeof builtin === "number" && target > builtin;
|
|
157
|
+
const exact = (n: number) => n.toLocaleString("en-US");
|
|
158
|
+
|
|
159
|
+
const begin = () => {
|
|
160
|
+
setEditing(true);
|
|
161
|
+
setText(formatCapacity(value));
|
|
162
|
+
setError(null);
|
|
163
|
+
setBuiltin(undefined);
|
|
164
|
+
const seq = ++infoSeq.current;
|
|
165
|
+
void postModels<{ builtin: number | null }>({ action: "contextWindowInfo", providerId: model.provider, modelId: model.id }).then(
|
|
166
|
+
(info) => { if (seq === infoSeq.current) setBuiltin(info.builtin); },
|
|
167
|
+
() => { if (seq === infoSeq.current) setBuiltin(null); },
|
|
168
|
+
);
|
|
169
|
+
};
|
|
170
|
+
const cancel = () => {
|
|
171
|
+
infoSeq.current += 1;
|
|
172
|
+
setEditing(false);
|
|
173
|
+
};
|
|
174
|
+
const save = async () => {
|
|
175
|
+
if (target === null || saving) return;
|
|
176
|
+
setSaving(true);
|
|
177
|
+
setError(null);
|
|
178
|
+
try {
|
|
179
|
+
const info = await postModels<{ contextWindow: number }>({ action: "setContextWindow", providerId: model.provider, modelId: model.id, contextWindow: target });
|
|
180
|
+
onSaved(info.contextWindow);
|
|
181
|
+
setEditing(false);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
184
|
+
} finally {
|
|
185
|
+
setSaving(false);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
if (!editing) {
|
|
190
|
+
return (
|
|
191
|
+
<div className="hairline-t mt-3 flex items-center justify-between gap-2 pt-3" style={{ color: "var(--dsw-label-tertiary)" }}>
|
|
192
|
+
<span>{t.contextWindowLabel}</span>
|
|
193
|
+
<span className="flex items-center gap-2">
|
|
194
|
+
<span style={{ color: "var(--dsw-label-secondary)", fontFamily: "var(--font-mono)" }}>{exact(value)}</span>
|
|
195
|
+
<button type="button" className="btn-outline" style={{ height: 22, padding: "0 8px", fontSize: 11.5 }} onClick={begin}>{t.contextWindowEdit}</button>
|
|
196
|
+
</span>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
196
200
|
return (
|
|
197
|
-
<div className="
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
<div className="hairline-t mt-3 pt-3" style={{ color: "var(--dsw-label-tertiary)" }}>
|
|
202
|
+
<div>{t.contextWindowLabel}</div>
|
|
203
|
+
<div className="mt-1.5 flex items-center gap-1.5">
|
|
204
|
+
<input
|
|
205
|
+
autoFocus
|
|
206
|
+
value={text}
|
|
207
|
+
onChange={(e) => setText(e.target.value)}
|
|
208
|
+
onKeyDown={(e) => {
|
|
209
|
+
if (e.key === "Enter") {
|
|
210
|
+
e.preventDefault();
|
|
211
|
+
void save();
|
|
212
|
+
} else if (e.key === "Escape") {
|
|
213
|
+
e.preventDefault();
|
|
214
|
+
e.stopPropagation();
|
|
215
|
+
cancel();
|
|
216
|
+
}
|
|
217
|
+
}}
|
|
218
|
+
aria-label={t.contextWindowLabel}
|
|
219
|
+
aria-invalid={!valid}
|
|
220
|
+
spellCheck={false}
|
|
221
|
+
className="min-w-0 flex-1 rounded-md px-2"
|
|
222
|
+
style={{ height: 26, fontSize: 12.5, fontFamily: "var(--font-mono)", color: "var(--dsw-label-primary)", background: "var(--dsw-hover)", border: `0.5px solid ${valid ? "var(--dsw-border-l2)" : "var(--dsw-danger)"}` }}
|
|
223
|
+
/>
|
|
224
|
+
<button type="button" className="btn-primary-white flex-none" style={{ height: 26, padding: "0 10px", fontSize: 12 }} disabled={!valid || saving} onClick={() => void save()}>
|
|
225
|
+
{saving ? t.saving : t.save}
|
|
226
|
+
</button>
|
|
227
|
+
<button type="button" className="btn-outline flex-none" style={{ height: 26, padding: "0 10px", fontSize: 12 }} disabled={saving} onClick={cancel}>{t.cancel}</button>
|
|
228
|
+
</div>
|
|
229
|
+
{typeof builtin === "number" && (
|
|
230
|
+
<div className="mt-1.5 flex items-center gap-1.5" style={{ fontSize: 11.5 }}>
|
|
231
|
+
<span>{t.contextWindowBuiltin.replace("{value}", exact(builtin))}</span>
|
|
232
|
+
{target !== builtin && (
|
|
233
|
+
<button type="button" className="underline" style={{ color: "var(--dsw-label-secondary)" }} onClick={() => setText(formatCapacity(builtin))}>{t.contextWindowUseBuiltin}</button>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
{!valid ? (
|
|
238
|
+
<div className="mt-1" style={{ fontSize: 11.5, color: "var(--dsw-danger)" }}>{t.contextWindowInvalid}</div>
|
|
239
|
+
) : over ? (
|
|
240
|
+
<div className="mt-1" style={{ fontSize: 11.5, color: "var(--dsw-warn)" }}>{t.contextWindowOverBuiltin}</div>
|
|
241
|
+
) : builtin === null ? (
|
|
242
|
+
<div className="mt-1" style={{ fontSize: 11.5 }}>{t.contextWindowNoBuiltin}</div>
|
|
243
|
+
) : null}
|
|
244
|
+
<div className="mt-1" style={{ fontSize: 11.5 }}>{t.contextWindowScope}</div>
|
|
245
|
+
{error && <div className="mt-1" role="alert" style={{ fontSize: 11.5, color: "var(--dsw-danger)" }}>{error}</div>}
|
|
203
246
|
</div>
|
|
204
247
|
);
|
|
205
248
|
}
|
|
249
|
+
|
|
250
|
+
function DataRow({ label, value, exact, color }: { label: string; value: string; exact?: string; color: string }) {
|
|
251
|
+
return <div className="flex items-center justify-between gap-2">
|
|
252
|
+
<span className="flex items-center gap-1.5"><span className="inline-block rounded-full" style={{ width: 7, height: 7, background: color }} />{label}</span>
|
|
253
|
+
<span title={exact}>{value}</span>
|
|
254
|
+
</div>;
|
|
255
|
+
}
|
|
@@ -39,10 +39,19 @@ interface GitInfo {
|
|
|
39
39
|
statusError?: string;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
interface GithubIdentity {
|
|
43
|
+
login: string;
|
|
44
|
+
name?: string;
|
|
45
|
+
avatar?: string;
|
|
46
|
+
url?: string;
|
|
47
|
+
repo?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
42
50
|
interface DiffResult {
|
|
43
51
|
patch: string;
|
|
44
52
|
truncated: boolean;
|
|
45
53
|
binary: boolean;
|
|
54
|
+
stats?: { add: number; del: number };
|
|
46
55
|
title?: string;
|
|
47
56
|
}
|
|
48
57
|
|
|
@@ -75,6 +84,7 @@ export function GitPanel({
|
|
|
75
84
|
}) {
|
|
76
85
|
const { t } = useI18n();
|
|
77
86
|
const [info, setInfo] = useState<GitInfo | null>(null);
|
|
87
|
+
const [identity, setIdentity] = useState<GithubIdentity | null>(null);
|
|
78
88
|
const [loading, setLoading] = useState(false);
|
|
79
89
|
const [statusError, setStatusError] = useState("");
|
|
80
90
|
const [diffError, setDiffError] = useState("");
|
|
@@ -105,6 +115,23 @@ export function GitPanel({
|
|
|
105
115
|
}
|
|
106
116
|
}, [cwd]);
|
|
107
117
|
|
|
118
|
+
// GitHub 身份单独拉:要走网络(gh / 头像),不能拖慢上面那条纯本地的状态读取
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
setIdentity(null);
|
|
121
|
+
if (!cwd) return;
|
|
122
|
+
const controller = new AbortController();
|
|
123
|
+
void (async () => {
|
|
124
|
+
try {
|
|
125
|
+
const r = await fetch(`/api/github?cwd=${encodeURIComponent(cwd)}`, { signal: controller.signal });
|
|
126
|
+
const j = await r.json();
|
|
127
|
+
if (j?.success && j.data?.identity) setIdentity(j.data.identity as GithubIdentity);
|
|
128
|
+
} catch {
|
|
129
|
+
/* 取不到就只显示分支,不报错 */
|
|
130
|
+
}
|
|
131
|
+
})();
|
|
132
|
+
return () => controller.abort();
|
|
133
|
+
}, [cwd, refreshKey]);
|
|
134
|
+
|
|
108
135
|
useEffect(() => {
|
|
109
136
|
setInfo(null);
|
|
110
137
|
setView(null);
|
|
@@ -165,13 +192,31 @@ export function GitPanel({
|
|
|
165
192
|
const error = view ? diffError : statusError || info?.statusError || "";
|
|
166
193
|
|
|
167
194
|
const parsed = useMemo(() => (diff?.patch ? parseUnifiedDiff(diff.patch) : null), [diff]);
|
|
168
|
-
|
|
195
|
+
// 新服务端给完整 Git 统计;旧服务端只在 patch 完整时才允许本地计数。
|
|
196
|
+
const stats = diff?.binary ? null : diff?.stats ?? (parsed && !diff?.truncated ? diffStats(parsed) : null);
|
|
169
197
|
|
|
170
198
|
const conflicts = info?.files.filter((f) => f.kind === "conflict") ?? [];
|
|
171
199
|
const staged = info?.files.filter((f) => f.kind !== "conflict" && f.kind !== "untracked" && f.indexStatus !== " ") ?? [];
|
|
172
200
|
const unstaged = info?.files.filter((f) => f.kind !== "conflict" && f.kind !== "untracked" && f.workStatus !== " ") ?? [];
|
|
173
201
|
const untracked = info?.files.filter((f) => f.kind === "untracked") ?? [];
|
|
174
202
|
|
|
203
|
+
/**
|
|
204
|
+
* 按 Git 的三个区呈现(分支 / 上游那些是「版本库」侧,在别的行):
|
|
205
|
+
* - 暂存区:`git add` 过的,下次 commit 会带上;
|
|
206
|
+
* - 工作区:改了还没 add 的 + 未跟踪的新文件(都属于「还没进暂存区」);
|
|
207
|
+
* - 冲突单独置顶(不先解决,提交走不下去)。
|
|
208
|
+
* 未跟踪文件要整份当新增行看,模式逐文件给,不能按组统一。
|
|
209
|
+
*/
|
|
210
|
+
const areas: Array<{ key: string; label: string; entries: Array<{ file: GitFileEntry; mode: "staged" | "worktree" | "untracked" }>; danger?: boolean }> = [
|
|
211
|
+
{ key: "conflict", label: t.gitConflict, entries: conflicts.map((file) => ({ file, mode: "worktree" as const })), danger: true },
|
|
212
|
+
{ key: "staged", label: t.gitStagingArea, entries: staged.map((file) => ({ file, mode: "staged" as const })) },
|
|
213
|
+
{
|
|
214
|
+
key: "worktree",
|
|
215
|
+
label: t.gitWorkingTree,
|
|
216
|
+
entries: [...unstaged.map((file) => ({ file, mode: "worktree" as const })), ...untracked.map((file) => ({ file, mode: "untracked" as const }))],
|
|
217
|
+
},
|
|
218
|
+
].filter((area) => area.entries.length > 0);
|
|
219
|
+
|
|
175
220
|
const openFile = (f: GitFileEntry, mode: "staged" | "worktree" | "untracked", label: string) => changeView({ kind: "file", path: f.path, mode, label });
|
|
176
221
|
|
|
177
222
|
const headerTitle = view
|
|
@@ -247,6 +292,39 @@ export function GitPanel({
|
|
|
247
292
|
)}
|
|
248
293
|
{!view && info?.isRepo && (
|
|
249
294
|
<div className="flex flex-col gap-4">
|
|
295
|
+
{/* GitHub 身份:头像 + 账号(取不到就整行不出现) */}
|
|
296
|
+
{identity && (
|
|
297
|
+
<div className="flex items-center gap-2">
|
|
298
|
+
{identity.avatar ? (
|
|
299
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
300
|
+
<img src={identity.avatar} alt="" width={22} height={22} className="flex-none rounded-full" />
|
|
301
|
+
) : (
|
|
302
|
+
<span
|
|
303
|
+
className="grid flex-none place-items-center rounded-full"
|
|
304
|
+
style={{ width: 22, height: 22, background: "var(--dsw-selector)", fontSize: 11, fontWeight: 600, color: "var(--dsw-label-secondary)" }}
|
|
305
|
+
aria-hidden
|
|
306
|
+
>
|
|
307
|
+
{identity.login.slice(0, 1).toUpperCase()}
|
|
308
|
+
</span>
|
|
309
|
+
)}
|
|
310
|
+
<a
|
|
311
|
+
href={identity.url ?? `https://github.com/${identity.login}`}
|
|
312
|
+
target="_blank"
|
|
313
|
+
rel="noreferrer"
|
|
314
|
+
className="min-w-0 truncate"
|
|
315
|
+
style={{ fontSize: 12.5, color: "var(--dsw-label-secondary)", textDecoration: "none" }}
|
|
316
|
+
title={identity.name ? `${identity.name} (@${identity.login})` : `@${identity.login}`}
|
|
317
|
+
>
|
|
318
|
+
@{identity.login}
|
|
319
|
+
</a>
|
|
320
|
+
{identity.repo && (
|
|
321
|
+
<span className="min-w-0 truncate" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }} title={identity.repo}>
|
|
322
|
+
· {identity.repo}
|
|
323
|
+
</span>
|
|
324
|
+
)}
|
|
325
|
+
</div>
|
|
326
|
+
)}
|
|
327
|
+
|
|
250
328
|
{/* 分支行 */}
|
|
251
329
|
<div className="flex flex-wrap items-center gap-2" style={{ fontSize: 12.5 }}>
|
|
252
330
|
<span
|
|
@@ -280,45 +358,37 @@ export function GitPanel({
|
|
|
280
358
|
{error ? null : info.files.length === 0 ? (
|
|
281
359
|
<div style={{ fontSize: 12.5, color: "var(--dsw-label-caption)" }}>{t.gitNoChanges}</div>
|
|
282
360
|
) : (
|
|
283
|
-
|
|
284
|
-
{
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
{
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
{f.path}
|
|
315
|
-
</span>
|
|
316
|
-
</button>
|
|
317
|
-
))}
|
|
318
|
-
</div>
|
|
319
|
-
</div>
|
|
320
|
-
),
|
|
321
|
-
)
|
|
361
|
+
areas.map((area) => (
|
|
362
|
+
<div key={area.key}>
|
|
363
|
+
<div className="mb-1 flex items-center gap-2" style={{ fontSize: 11.5, color: area.danger ? "var(--dsw-danger)" : "var(--dsw-label-caption)" }}>
|
|
364
|
+
<span style={{ fontWeight: 600 }}>{area.label}</span>
|
|
365
|
+
<span>{area.entries.length}</span>
|
|
366
|
+
</div>
|
|
367
|
+
<div className="flex flex-col">
|
|
368
|
+
{area.entries.map(({ file: f, mode }) => (
|
|
369
|
+
<button
|
|
370
|
+
key={`${area.key}:${f.path}`}
|
|
371
|
+
type="button"
|
|
372
|
+
className="flex w-full items-center gap-2 rounded-lg px-2 py-1 text-left transition-colors"
|
|
373
|
+
title={f.path}
|
|
374
|
+
onMouseEnter={(e) => (e.currentTarget.style.background = "var(--dsw-hover)")}
|
|
375
|
+
onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}
|
|
376
|
+
onClick={() => openFile(f, mode, mode === "staged" ? t.gitStagedDiff : mode === "untracked" ? t.gitUntracked : t.gitWorktreeDiff)}
|
|
377
|
+
>
|
|
378
|
+
<span
|
|
379
|
+
className="flex-none rounded px-1 text-center"
|
|
380
|
+
style={{ minWidth: 16, fontSize: 10.5, fontFamily: "var(--font-mono)", fontWeight: 600, color: KIND_COLOR[f.kind], background: "var(--dsw-hover)" }}
|
|
381
|
+
>
|
|
382
|
+
{f.kind === "untracked" ? "?" : f.kind === "conflict" ? "!" : mode === "staged" ? f.indexStatus : f.workStatus !== " " ? f.workStatus : f.indexStatus}
|
|
383
|
+
</span>
|
|
384
|
+
<span className="min-w-0 flex-1 truncate" style={{ fontSize: 12, fontFamily: "var(--font-mono)", color: "var(--dsw-label-secondary)" }}>
|
|
385
|
+
{f.path}
|
|
386
|
+
</span>
|
|
387
|
+
</button>
|
|
388
|
+
))}
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
))
|
|
322
392
|
)}
|
|
323
393
|
|
|
324
394
|
{/* 最近提交 */}
|