@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
|
@@ -6,18 +6,23 @@
|
|
|
6
6
|
* 运行中:Enter 按设置插话 / 排队,Ctrl(⌘)+Enter 用另一种;停止按钮会把排队消息取回输入框(pi 终端同款);
|
|
7
7
|
* 排队中的消息列在输入卡上方,↑(空输入框时)取回编辑。Enter 发送 / Shift+Enter 换行。
|
|
8
8
|
*/
|
|
9
|
-
import { useEffect, useRef, useState } from "react";
|
|
9
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
10
10
|
import {
|
|
11
11
|
IconAtOutline16,
|
|
12
12
|
IconFileOutline16,
|
|
13
|
+
IconFolderClose16,
|
|
13
14
|
IconSendArrowUp14,
|
|
14
15
|
IconStopFill16,
|
|
15
16
|
IconTerminalOutline14,
|
|
16
17
|
} from "@/components/icons";
|
|
17
18
|
import { ContextMeter } from "@/components/ContextMeter";
|
|
19
|
+
import { COLLAPSE_AT, ComposerImages } from "@/components/composer/ComposerImages";
|
|
18
20
|
import { ModelSelector, type ModelChoice } from "@/components/ModelSelector";
|
|
19
21
|
import { useI18n } from "@/i18n";
|
|
22
|
+
import { useWorkspaceFiles } from "@/hooks/useWorkspaceFiles";
|
|
23
|
+
import { fmtBytes } from "@/lib/format";
|
|
20
24
|
import { otherBehavior, useEnterBehavior } from "@/lib/enter-behavior";
|
|
25
|
+
import { atToken, candidateText, suggestPaths, type PathCandidate } from "@/lib/workspace-index";
|
|
21
26
|
import type { ImageAttachment } from "@/lib/types";
|
|
22
27
|
|
|
23
28
|
export interface SlashCommand {
|
|
@@ -55,6 +60,7 @@ export function ChatInput({
|
|
|
55
60
|
contextWindow,
|
|
56
61
|
contextSource,
|
|
57
62
|
contextVisible = true,
|
|
63
|
+
cwd,
|
|
58
64
|
model,
|
|
59
65
|
thinkingLevel,
|
|
60
66
|
thinkingLevels,
|
|
@@ -89,12 +95,14 @@ export function ChatInput({
|
|
|
89
95
|
/** 上下文分段数据源:ContextMeter 弹窗打开时才做字符统计 */
|
|
90
96
|
contextSource?: {
|
|
91
97
|
systemChars: number;
|
|
92
|
-
messages: { role?: string; content: Array<{ type: string; text?: string; thinking?: string }
|
|
98
|
+
messages: { role?: string; content: Array<{ type: string; text?: string; thinking?: string; arguments?: unknown; name?: string }>; usage?: import("@/lib/types").TrajTokens; stopReason?: string }[];
|
|
93
99
|
breakdown?: import("@/lib/types").ContextBreakdown;
|
|
94
100
|
loadFull?: () => Promise<unknown> | unknown;
|
|
95
101
|
};
|
|
96
102
|
/** 还没有对话时隐藏上下文计量 */
|
|
97
103
|
contextVisible?: boolean;
|
|
104
|
+
/** 工作区根:@ 引用的候选清单从这里取(不传就不启用 @ 引用) */
|
|
105
|
+
cwd?: string;
|
|
98
106
|
model?: { provider: string; id: string; name: string };
|
|
99
107
|
thinkingLevel?: string;
|
|
100
108
|
thinkingLevels: string[];
|
|
@@ -149,10 +157,18 @@ export function ChatInput({
|
|
|
149
157
|
const uploadBusy = uploadCount > 0 || pendingUploadCount > 0;
|
|
150
158
|
const sendBusy = sending || pendingSend;
|
|
151
159
|
const [dragActive, setDragActive] = useState(false);
|
|
160
|
+
/** 引用 chip 超过 COLLAPSE_AT 就先折叠:手机上折行会把输入框顶掉(图片的折叠在 ComposerImages 里) */
|
|
161
|
+
const [refsExpanded, setRefsExpanded] = useState(false);
|
|
152
162
|
const dragDepth = useRef(0);
|
|
153
163
|
const [modelOpen, setModelOpen] = useState(false);
|
|
154
164
|
const [cmdIdx, setCmdIdx] = useState(0);
|
|
155
165
|
const [cmdDismissed, setCmdDismissed] = useState(false);
|
|
166
|
+
// @ 引用:打字触发候选,选中后落成引用 chip(发送时统一拼成 `@路径` 行)
|
|
167
|
+
const [mention, setMention] = useState<{ query: string; start: number; tokenEnd: number } | null>(null);
|
|
168
|
+
const [mentionIdx, setMentionIdx] = useState(0);
|
|
169
|
+
/** 被 Esc 关掉的那个 token(位置 + 原文);光标在它里面移动不再弹,改了字、或换成别的 token 重新弹 */
|
|
170
|
+
const mentionDismissed = useRef<string | null>(null);
|
|
171
|
+
const mentionMenuRef = useRef<HTMLDivElement>(null);
|
|
156
172
|
const taRef = useRef<HTMLTextAreaElement>(null);
|
|
157
173
|
const wrapRef = useRef<HTMLDivElement>(null);
|
|
158
174
|
const { t } = useI18n();
|
|
@@ -179,6 +195,79 @@ export function ChatInput({
|
|
|
179
195
|
const filteredCommands = (commands ?? []).filter((c) => c.name.toLowerCase().startsWith(slash.split(" ")[0].toLowerCase()));
|
|
180
196
|
const visibleCommands = filteredCommands;
|
|
181
197
|
const commandMode = typedCommandMode;
|
|
198
|
+
|
|
199
|
+
// ---------- @ 引用:输入 @ 弹候选,选中落成引用 chip ----------
|
|
200
|
+
// 候选清单一次拉全量(工作区 git 可见文件),之后每个字符都在本地匹配;规则与 pi 终端的 @ 补全一致(见 workspace-index)。
|
|
201
|
+
const mentionMode = mention !== null;
|
|
202
|
+
const mentionQuery = mention?.query ?? null;
|
|
203
|
+
const files = useWorkspaceFiles(cwd ?? "", mentionMode);
|
|
204
|
+
// 只在候选清单或查询变了时重算:光标移动、其它状态变化引起的重渲染不该每次扫一遍全量清单
|
|
205
|
+
const suggestions = useMemo(
|
|
206
|
+
() => (mentionQuery === null ? [] : suggestPaths(files.candidates, mentionQuery)),
|
|
207
|
+
[files.candidates, mentionQuery],
|
|
208
|
+
);
|
|
209
|
+
const effectiveMentionIdx = Math.min(mentionIdx, Math.max(0, suggestions.length - 1));
|
|
210
|
+
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
mentionMenuRef.current?.querySelector('[aria-selected="true"]')?.scrollIntoView({ block: "nearest" });
|
|
213
|
+
}, [mentionIdx, suggestions.length]);
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 文本 / 光标变了就重新判是否在 @ token 里(Esc 关过的同一个 token 不再弹)。
|
|
217
|
+
* 没有工作区就不弹:没有候选来源,菜单只会空转还拦着方向键和 Esc。
|
|
218
|
+
*/
|
|
219
|
+
const syncMention = (value: string, caret: number | null) => {
|
|
220
|
+
const token = caret === null || !cwd ? null : atToken(value, caret);
|
|
221
|
+
if (!token) {
|
|
222
|
+
// 离开 token 就忘掉 Esc:之后在同一位置打新的 @ 照常弹
|
|
223
|
+
mentionDismissed.current = null;
|
|
224
|
+
setMention(null);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (mentionDismissed.current === `${token.start}:${value.slice(token.start, token.tokenEnd)}`) {
|
|
228
|
+
setMention(null);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (!mention || mention.start !== token.start || mention.query !== token.query) setMentionIdx(0);
|
|
232
|
+
if (!mention || mention.start !== token.start || mention.query !== token.query || mention.tokenEnd !== token.tokenEnd) {
|
|
233
|
+
setMention({ query: token.query, start: token.start, tokenEnd: token.tokenEnd });
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const closeMention = (dismiss: boolean) => {
|
|
238
|
+
if (dismiss && mention) mentionDismissed.current = `${mention.start}:${text.slice(mention.start, mention.tokenEnd)}`;
|
|
239
|
+
setMention(null);
|
|
240
|
+
setMentionIdx(0);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/** 选中候选:目录引用文件夹本身(要往下钻就继续打 `@src/lib/`),token 从正文里播掉 */
|
|
244
|
+
const applyMention = (candidate: PathCandidate) => {
|
|
245
|
+
if (!mention) return;
|
|
246
|
+
const { start, tokenEnd } = mention;
|
|
247
|
+
// 目录存成 `src/lib/`:chip 靠尾斜杠判断文件夹(去重会把尾斜杠归一化掉)
|
|
248
|
+
const path = candidate.dir ? `${candidate.path}/` : candidate.path;
|
|
249
|
+
changeDraft((previous) => {
|
|
250
|
+
const refs = previous.references ?? [];
|
|
251
|
+
// 目录带尾斜杠、文件不带:去重按去掉尾斜杠比较,
|
|
252
|
+
// 否则「文件面板引用的 src/lib」与「@ 选的 src/lib/」会变成两个 chip
|
|
253
|
+
const samePath = (a: string, b: string) => a.replace(/[\\/]+$/, "") === b.replace(/[\\/]+$/, "");
|
|
254
|
+
return {
|
|
255
|
+
...previous,
|
|
256
|
+
text: previous.text.slice(0, start) + previous.text.slice(tokenEnd),
|
|
257
|
+
references: refs.some((existing) => samePath(existing, path)) ? refs : [...refs, path],
|
|
258
|
+
};
|
|
259
|
+
});
|
|
260
|
+
setMention(null);
|
|
261
|
+
setMentionIdx(0);
|
|
262
|
+
requestAnimationFrame(() => {
|
|
263
|
+
autoSize();
|
|
264
|
+
const ta = taRef.current;
|
|
265
|
+
if (ta) {
|
|
266
|
+
ta.focus();
|
|
267
|
+
ta.setSelectionRange(start, start);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
};
|
|
182
271
|
// 命令菜单滚动:.popover 的 overflow:hidden 会压掉 overflow-y-auto(unlayered CSS 优先),
|
|
183
272
|
// 内联 overflowY 强制可滚;键盘导航时把选中项滚进视野
|
|
184
273
|
const cmdMenuRef = useRef<HTMLDivElement>(null);
|
|
@@ -445,6 +534,63 @@ export function ChatInput({
|
|
|
445
534
|
{attachmentError}
|
|
446
535
|
</div>
|
|
447
536
|
)}
|
|
537
|
+
{/* @ 引用候选(与斜杠菜单同一处锚点:输入卡上方) */}
|
|
538
|
+
{mentionMode && (
|
|
539
|
+
<div
|
|
540
|
+
ref={mentionMenuRef}
|
|
541
|
+
data-testid="mention-menu"
|
|
542
|
+
className="popover absolute bottom-full left-0 right-0 z-50 mb-2 max-h-[340px] overflow-y-auto py-1.5"
|
|
543
|
+
role="listbox"
|
|
544
|
+
aria-label={t.mentionFiles}
|
|
545
|
+
style={{ overflowY: "auto" }}
|
|
546
|
+
>
|
|
547
|
+
<div className="flex items-center gap-2 px-4 pb-1 pt-1" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
|
|
548
|
+
<span>{t.mentionFiles}</span>
|
|
549
|
+
{files.truncated && <span style={{ color: "var(--dsw-warn)" }}>{t.mentionTruncated}</span>}
|
|
550
|
+
{files.source === "disk" && <span title={t.mentionDiskSourceHint}>{t.mentionDiskSource}</span>}
|
|
551
|
+
<span className="flex-1" />
|
|
552
|
+
<span>{t.mentionHint}</span>
|
|
553
|
+
</div>
|
|
554
|
+
{files.status === "loading" && (
|
|
555
|
+
<div className="px-4 py-2" style={{ fontSize: 12.5, color: "var(--dsw-label-caption)" }}>{t.panelLoading}</div>
|
|
556
|
+
)}
|
|
557
|
+
{files.status === "error" && (
|
|
558
|
+
<div className="px-4 py-2" style={{ fontSize: 12.5, color: "var(--dsw-danger)" }} role="alert">{files.error}</div>
|
|
559
|
+
)}
|
|
560
|
+
{files.status === "ready" && suggestions.length === 0 && (
|
|
561
|
+
<div className="px-4 py-2" style={{ fontSize: 12.5, color: "var(--dsw-label-caption)" }}>{t.mentionNoMatch}</div>
|
|
562
|
+
)}
|
|
563
|
+
{suggestions.map(({ candidate, label, description }, i) => (
|
|
564
|
+
<button
|
|
565
|
+
key={`${candidate.dir ? "d" : "f"}:${candidate.path}`}
|
|
566
|
+
type="button"
|
|
567
|
+
role="option"
|
|
568
|
+
aria-selected={i === effectiveMentionIdx}
|
|
569
|
+
className="flex w-full items-center gap-2 px-4 py-1.5 text-left transition-colors"
|
|
570
|
+
style={{ background: i === effectiveMentionIdx ? "var(--dsw-hover)" : "transparent" }}
|
|
571
|
+
onMouseEnter={() => setMentionIdx(i)}
|
|
572
|
+
onClick={() => applyMention(candidate)}
|
|
573
|
+
title={candidateText(candidate)}
|
|
574
|
+
>
|
|
575
|
+
{candidate.dir ? (
|
|
576
|
+
<IconFolderClose16 size={13} style={{ flex: "none", color: "var(--dsw-warn)" }} />
|
|
577
|
+
) : (
|
|
578
|
+
<IconFileOutline16 size={13} style={{ flex: "none", color: "var(--dsw-label-caption)" }} />
|
|
579
|
+
)}
|
|
580
|
+
{/* 和 pi 终端一样:名字在前,路径在后;根目录下的条目两者相同,不重复显示 */}
|
|
581
|
+
<span className="flex-none truncate" style={{ maxWidth: "55%", fontSize: 12.5, fontFamily: "var(--font-mono)", color: "var(--dsw-label-primary)" }}>
|
|
582
|
+
{label}
|
|
583
|
+
</span>
|
|
584
|
+
{description.replace(/\/$/, "") !== label.replace(/\/$/, "") && (
|
|
585
|
+
<span className="min-w-0 flex-1 truncate" style={{ fontSize: 11.5, fontFamily: "var(--font-mono)", color: "var(--dsw-label-caption)" }}>
|
|
586
|
+
{description}
|
|
587
|
+
</span>
|
|
588
|
+
)}
|
|
589
|
+
</button>
|
|
590
|
+
))}
|
|
591
|
+
</div>
|
|
592
|
+
)}
|
|
593
|
+
|
|
448
594
|
{/* 斜杠命令菜单(dsh:悬浮于输入卡上方,同宽) */}
|
|
449
595
|
{commandMode && visibleCommands.length > 0 && (
|
|
450
596
|
<div ref={cmdMenuRef} className="popover absolute bottom-full left-0 right-0 z-50 mb-2 max-h-[340px] overflow-y-auto py-1.5" role="listbox" aria-label={t.slashCatalog} style={{ overflowY: "auto" }}>
|
|
@@ -478,65 +624,62 @@ export function ChatInput({
|
|
|
478
624
|
</div>
|
|
479
625
|
)}
|
|
480
626
|
|
|
481
|
-
{/*
|
|
627
|
+
{/* 附件轨:图片(超过 4 张折叠成牌堆,展开沿弧线扇开;点开可放大、切换、删除) */}
|
|
482
628
|
{images.length > 0 && (
|
|
483
|
-
<
|
|
484
|
-
{images
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
alt=""
|
|
489
|
-
className="h-14 w-14 rounded-lg object-cover"
|
|
490
|
-
style={{ border: "0.5px solid var(--dsw-border-l2)" }}
|
|
491
|
-
/>
|
|
492
|
-
<button
|
|
493
|
-
className="absolute -right-1.5 -top-1.5 flex h-5 w-5 items-center justify-center rounded-full text-[10px]"
|
|
494
|
-
style={{ background: "var(--dsw-label-primary)", color: "var(--dsw-bg-base)" }}
|
|
495
|
-
disabled={sendBusy}
|
|
496
|
-
onClick={() => changeDraft((previous) => ({ ...previous, images: previous.images.filter((_, index) => index !== i) }))}
|
|
497
|
-
>
|
|
498
|
-
✕
|
|
499
|
-
</button>
|
|
500
|
-
</div>
|
|
501
|
-
))}
|
|
502
|
-
</div>
|
|
629
|
+
<ComposerImages
|
|
630
|
+
images={images}
|
|
631
|
+
disabled={sendBusy}
|
|
632
|
+
onRemove={(index) => changeDraft((previous) => ({ ...previous, images: previous.images.filter((_, at) => at !== index) }))}
|
|
633
|
+
/>
|
|
503
634
|
)}
|
|
504
635
|
|
|
505
|
-
{/* 附件轨:上传的普通文件(压缩包/文档等,路径引用) */}
|
|
506
636
|
{/* 路径引用(项目结构图/文件面板的「引用到输入框」):独立于正文,发送时拼成 @路径 */}
|
|
507
637
|
{references.length > 0 && (
|
|
508
638
|
<div className="mb-2 flex flex-wrap gap-1.5 px-1" data-testid="composer-references">
|
|
509
|
-
{references.map((path) =>
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
<span className="truncate">{referenceLabelOf(path)}</span>
|
|
525
|
-
<button
|
|
526
|
-
type="button"
|
|
527
|
-
className="flex h-4 w-4 flex-none items-center justify-center rounded-full text-[10px]"
|
|
528
|
-
style={{ color: "var(--dsw-label-caption)" }}
|
|
529
|
-
disabled={sendBusy}
|
|
530
|
-
onClick={() => changeDraft((previous) => ({ ...previous, references: (previous.references ?? []).filter((p) => p !== path) }))}
|
|
531
|
-
aria-label={`${t.delete} ${path}`}
|
|
639
|
+
{(refsExpanded ? references : references.slice(0, COLLAPSE_AT)).map((path) => {
|
|
640
|
+
const chip = referenceChipOf(path);
|
|
641
|
+
return (
|
|
642
|
+
<span
|
|
643
|
+
key={path}
|
|
644
|
+
className="flex items-center gap-1 rounded-md px-2 py-1"
|
|
645
|
+
style={{
|
|
646
|
+
maxWidth: 260,
|
|
647
|
+
fontSize: 12,
|
|
648
|
+
fontFamily: "var(--font-mono)",
|
|
649
|
+
color: "var(--dsw-label-secondary)",
|
|
650
|
+
background: "var(--dsw-selector)",
|
|
651
|
+
border: "0.5px solid var(--dsw-border-l3)",
|
|
652
|
+
}}
|
|
653
|
+
title={path}
|
|
532
654
|
>
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
655
|
+
{chip.dir ? (
|
|
656
|
+
<IconFolderClose16 size={12} style={{ flex: "none", color: "var(--dsw-warn)" }} />
|
|
657
|
+
) : (
|
|
658
|
+
<IconFileOutline16 size={12} style={{ flex: "none", color: "var(--dsw-accent)" }} />
|
|
659
|
+
)}
|
|
660
|
+
<span className="truncate">{chip.text}</span>
|
|
661
|
+
<button
|
|
662
|
+
type="button"
|
|
663
|
+
className="flex h-4 w-4 flex-none items-center justify-center rounded-full text-[10px]"
|
|
664
|
+
style={{ color: "var(--dsw-label-caption)" }}
|
|
665
|
+
disabled={sendBusy}
|
|
666
|
+
onClick={() => changeDraft((previous) => ({ ...previous, references: (previous.references ?? []).filter((p) => p !== path) }))}
|
|
667
|
+
aria-label={`${t.delete} ${path}`}
|
|
668
|
+
>
|
|
669
|
+
✕
|
|
670
|
+
</button>
|
|
671
|
+
</span>
|
|
672
|
+
);
|
|
673
|
+
})}
|
|
674
|
+
{references.length > COLLAPSE_AT ? (
|
|
675
|
+
<button type="button" className="pw-chip" data-testid="references-toggle" onClick={() => setRefsExpanded((open) => !open)}>
|
|
676
|
+
{refsExpanded ? t.chipCollapse : t.chipExpandMore.replace("{n}", String(references.length - COLLAPSE_AT))}
|
|
677
|
+
</button>
|
|
678
|
+
) : null}
|
|
537
679
|
</div>
|
|
538
680
|
)}
|
|
539
681
|
|
|
682
|
+
{/* 附件轨:上传的普通文件(压缩包/文档等,路径引用) */}
|
|
540
683
|
{uploads.length > 0 && (
|
|
541
684
|
<div className="mb-2 flex flex-wrap gap-2 px-1">
|
|
542
685
|
{uploads.map((f, i) => (
|
|
@@ -605,12 +748,19 @@ export function ChatInput({
|
|
|
605
748
|
suppressHydrationWarning
|
|
606
749
|
className="min-w-0 flex-1 resize-none"
|
|
607
750
|
style={{ fontSize: "var(--piweb-chat-font-size, var(--dsh-content-font-size))", lineHeight: 1.55 }}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
751
|
+
onChange={(e) => {
|
|
752
|
+
const value = e.target.value;
|
|
753
|
+
setText(value);
|
|
754
|
+
setCmdDismissed(false);
|
|
611
755
|
setCmdIdx(0);
|
|
756
|
+
syncMention(value, e.target.selectionStart);
|
|
612
757
|
autoSize();
|
|
613
758
|
}}
|
|
759
|
+
onSelect={(e) => {
|
|
760
|
+
// 选中了一段文字时不弹:那时 Enter / Tab 应该照常工作
|
|
761
|
+
const ta = e.target as HTMLTextAreaElement;
|
|
762
|
+
syncMention(ta.value, ta.selectionStart === ta.selectionEnd ? ta.selectionStart : null);
|
|
763
|
+
}}
|
|
614
764
|
onPaste={(e) => {
|
|
615
765
|
const files = Array.from(e.clipboardData.files);
|
|
616
766
|
if (files.length) {
|
|
@@ -619,6 +769,34 @@ export function ChatInput({
|
|
|
619
769
|
}
|
|
620
770
|
}}
|
|
621
771
|
onKeyDown={(e) => {
|
|
772
|
+
// 输入法组字时的按键归输入法(Esc 取消组字、↑↓ 选字、Enter 上屏),一律不当候选操作
|
|
773
|
+
if (mentionMode && !e.nativeEvent.isComposing) {
|
|
774
|
+
// 候选开着时优先于发送:Esc 关掉、↑↓ 选、Enter / Tab 采纳。
|
|
775
|
+
// 没有候选时不拦 Enter——`@某人` 这种不成候选的文字要能直接发出去。
|
|
776
|
+
if (e.key === "Escape") {
|
|
777
|
+
e.preventDefault();
|
|
778
|
+
closeMention(true);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (suggestions.length > 0) {
|
|
782
|
+
if (e.key === "ArrowDown") {
|
|
783
|
+
e.preventDefault();
|
|
784
|
+
setMentionIdx((i) => Math.min(i + 1, suggestions.length - 1));
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
if (e.key === "ArrowUp") {
|
|
788
|
+
e.preventDefault();
|
|
789
|
+
setMentionIdx((i) => Math.max(i - 1, 0));
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (e.key === "Enter" || e.key === "Tab") {
|
|
793
|
+
e.preventDefault();
|
|
794
|
+
const picked = suggestions[effectiveMentionIdx];
|
|
795
|
+
if (picked) applyMention(picked.candidate);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
622
800
|
if (adopted && text === "" && (e.key === "Backspace" || e.key === "Escape")) {
|
|
623
801
|
// 空参数时退格/Esc:摘掉命令胶囊
|
|
624
802
|
e.preventDefault();
|
|
@@ -711,6 +889,7 @@ export function ChatInput({
|
|
|
711
889
|
percent={contextPercent}
|
|
712
890
|
tokens={contextTokens}
|
|
713
891
|
contextWindow={contextWindow}
|
|
892
|
+
model={model}
|
|
714
893
|
source={contextSource}
|
|
715
894
|
/>
|
|
716
895
|
)}
|
|
@@ -773,17 +952,19 @@ export function ChatInput({
|
|
|
773
952
|
}
|
|
774
953
|
|
|
775
954
|
function fmtUploadSize(bytes: number): string {
|
|
776
|
-
|
|
777
|
-
if (bytes >= 1024) return `${Math.round(bytes / 1024)}KB`;
|
|
778
|
-
return `${bytes}B`;
|
|
955
|
+
return fmtBytes(bytes).replace(/ /g, "");
|
|
779
956
|
}
|
|
780
957
|
|
|
781
958
|
/**
|
|
782
|
-
* 引用 chip
|
|
783
|
-
*
|
|
959
|
+
* 引用 chip 上显示的短标签。只取 basename 会分不清两件事:目录与同名文件
|
|
960
|
+
* (`src/lib` 与 `src/lib.ts` 都显示 `lib`;`a/index.ts` 与 `b/index.ts` 都显示 `index.ts`)。
|
|
961
|
+
* 规则:目录带尾斜杠(`lib/`,配文件夹图标);其余最多保留**尾部两段**
|
|
962
|
+
* (`src/lib/index.ts` → `…/lib/index.ts`)。完整路径仍在 title 里。
|
|
784
963
|
*/
|
|
785
|
-
function
|
|
786
|
-
const
|
|
787
|
-
const
|
|
788
|
-
|
|
964
|
+
function referenceChipOf(path: string): { text: string; dir: boolean } {
|
|
965
|
+
const dir = /[\\/]$/.test(path);
|
|
966
|
+
const parts = path.replace(/[\\/]+$/, "").split(/[\\/]/).filter(Boolean);
|
|
967
|
+
if (!parts.length) return { text: path, dir };
|
|
968
|
+
const tail = parts.slice(-2).join("/");
|
|
969
|
+
return { text: dir ? `${tail}/` : parts.length > 2 ? `…/${tail}` : tail, dir };
|
|
789
970
|
}
|
|
@@ -30,6 +30,7 @@ import { copyText } from "@/lib/clipboard";
|
|
|
30
30
|
import { OutlineRail } from "@/components/OutlineRail";
|
|
31
31
|
import { ChatScrollContext, type ChatScroll } from "@/components/chat/chat-scroll";
|
|
32
32
|
import { useI18n } from "@/i18n";
|
|
33
|
+
import { fmtDuration, fmtPercent, fmtThroughput, fmtTokens } from "@/lib/format";
|
|
33
34
|
import type { ToolCardState } from "@/hooks/usePiWeb";
|
|
34
35
|
import type { ContextResource, TrajEntry, TrajTokens, WebContent, WebMessage, WebStats } from "@/lib/types";
|
|
35
36
|
import {
|
|
@@ -128,16 +129,16 @@ function TurnMetaPill({ durationMs, usage, t }: { durationMs: number; usage?: Tr
|
|
|
128
129
|
<>
|
|
129
130
|
<div className="flex items-center justify-between gap-2">
|
|
130
131
|
<span style={{ color: "var(--dsw-label-caption)" }}>{t.inputTokens}</span>
|
|
131
|
-
<span>{
|
|
132
|
+
<span>{fmtTokens(billed)}</span>
|
|
132
133
|
</div>
|
|
133
134
|
<div className="flex items-center justify-between gap-2">
|
|
134
135
|
<span style={{ color: "var(--dsw-label-caption)" }}>{t.outputTokens}</span>
|
|
135
|
-
<span>{
|
|
136
|
+
<span>{fmtTokens(usage.output ?? 0)}</span>
|
|
136
137
|
</div>
|
|
137
138
|
{(usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0) > 0 && (
|
|
138
139
|
<div className="flex items-center justify-between gap-2">
|
|
139
140
|
<span style={{ color: "var(--dsw-label-caption)" }}>{t.cacheHits}</span>
|
|
140
|
-
<span>{
|
|
141
|
+
<span>{fmtPercent(((usage.cacheRead ?? 0) / Math.max(billed, 1)) * 100)}</span>
|
|
141
142
|
</div>
|
|
142
143
|
)}
|
|
143
144
|
</>
|
|
@@ -710,7 +711,7 @@ function WorkingStep({ startedAt, outputTokens, workingMessage, hasLiveStep }: {
|
|
|
710
711
|
<div className="pw-line">
|
|
711
712
|
{label ? <span className="pw-shimmer">{label}</span> : <span className="pw-verb">{fillBold(WORKING_FOR, { d: fmtSpan(elapsed) })}</span>}
|
|
712
713
|
{label && <span className="pw-meta">{fmtSpan(elapsed)}</span>}
|
|
713
|
-
{outputTokens ? <span className="pw-meta">↓ {
|
|
714
|
+
{outputTokens ? <span className="pw-meta">↓ {fmtTokens(outputTokens)} tokens</span> : null}
|
|
714
715
|
</div>
|
|
715
716
|
</Step>
|
|
716
717
|
);
|
|
@@ -1243,22 +1244,6 @@ function TurnLite({ turn, messages }: { turn: Turn; messages: WebMessage[] }) {
|
|
|
1243
1244
|
);
|
|
1244
1245
|
}
|
|
1245
1246
|
|
|
1246
|
-
function fmtTok(n: number): string {
|
|
1247
|
-
const scaled = (value: number) => value >= 100 ? String(Math.round(value)) : String(Math.round(value * 10) / 10);
|
|
1248
|
-
return n >= 1_000_000 ? `${scaled(n / 1_000_000)}M` : n >= 1000 ? `${scaled(n / 1000)}K` : String(n);
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
function fmtDuration(ms: number): string {
|
|
1252
|
-
const seconds = ms / 1000;
|
|
1253
|
-
if (seconds < 60) return `${Math.round(seconds * 10) / 10}s`;
|
|
1254
|
-
const whole = Math.round(seconds);
|
|
1255
|
-
return `${Math.floor(whole / 60)}m${whole % 60}s`;
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
function fmtThroughput(tokensPerSecond: number): string {
|
|
1259
|
-
return tokensPerSecond >= 10 ? String(Math.round(tokensPerSecond)) : String(Math.round(tokensPerSecond * 10) / 10);
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
1247
|
export function SessionStatsBar({
|
|
1263
1248
|
stats,
|
|
1264
1249
|
}: {
|
|
@@ -1272,24 +1257,30 @@ export function SessionStatsBar({
|
|
|
1272
1257
|
.replace("{steps}", String(stats.assistantMessages)),
|
|
1273
1258
|
];
|
|
1274
1259
|
const durations: string[] = [];
|
|
1275
|
-
|
|
1276
|
-
|
|
1260
|
+
const coverage = (n: number | undefined, total: number) => n !== undefined && n < total
|
|
1261
|
+
? ` (${t.statsTimedCoverage.replace("{n}", String(n)).replace("{total}", String(total))})` : "";
|
|
1262
|
+
if (stats.llmMs > 0) durations.push(`LLM ${fmtDuration(stats.llmMs)}${coverage(stats.llmTimedSteps, stats.assistantMessages)}`);
|
|
1263
|
+
if (stats.toolMs > 0) durations.push(`${t.toolCalls} ${fmtDuration(stats.toolMs)}${coverage(stats.toolTimedCalls, stats.toolCalls)}`);
|
|
1277
1264
|
if (durations.length) groups.push(durations.join(" · "));
|
|
1278
1265
|
const speeds: string[] = [];
|
|
1279
|
-
|
|
1280
|
-
if (stats.
|
|
1266
|
+
const timedLabel = (steps: number) => ` (${t.statsMeasuredSteps.replace("{n}", String(steps))})`;
|
|
1267
|
+
if (stats.ttftSteps > 0) speeds.push(`${t.ttftAverage} ${fmtDuration(stats.ttftMs / stats.ttftSteps)}${timedLabel(stats.ttftSteps)}`);
|
|
1268
|
+
if (stats.decodeMs > 0) speeds.push(`${fmtThroughput(stats.decodeTokens / (stats.decodeMs / 1000))} tok/s${timedLabel(stats.decodeSteps ?? stats.ttftSteps)}`);
|
|
1281
1269
|
if (speeds.length) groups.push(speeds.join(" · "));
|
|
1282
1270
|
const billedInput = stats.tokens.input + stats.tokens.cacheRead + stats.tokens.cacheWrite;
|
|
1283
1271
|
if (billedInput > 0 || stats.tokens.output > 0) {
|
|
1284
|
-
if (billedInput > 0) groups.push(`${t.cacheHits} ${
|
|
1285
|
-
groups.push(`${t.inputTokens} ${
|
|
1272
|
+
if (billedInput > 0) groups.push(`${t.cacheHits} ${fmtPercent((stats.tokens.cacheRead / billedInput) * 100)}`);
|
|
1273
|
+
groups.push(`${t.inputTokens} ${fmtTokens(billedInput)} tok · ${t.outputTokens} ${fmtTokens(stats.tokens.output)} tok`);
|
|
1286
1274
|
}
|
|
1287
1275
|
const line = groups.join(" | ");
|
|
1288
1276
|
return (
|
|
1289
1277
|
// 悬停显示完整统计:自定义浮层固定在统计条上方(原生 title 位置不可控)
|
|
1278
|
+
// 容器只有 544px(内容窄轴下限)而整行实测 959px,以前一律省略号截断:
|
|
1279
|
+
// 提高精度后更长,「输入 / 输出 tokens」这类用户最关心的反而看不到。
|
|
1280
|
+
// 改成允许换行:窄屏折成两行完整显示,代价是纵向多占 20px。
|
|
1290
1281
|
<div className="group relative mx-auto mt-1 block w-full px-4 pt-1 text-center">
|
|
1291
1282
|
<div
|
|
1292
|
-
className="overflow-
|
|
1283
|
+
className="[overflow-wrap:anywhere]"
|
|
1293
1284
|
style={{ fontSize: "var(--dsh-content-font-size-secondary, 13px)", lineHeight: "20px", color: "var(--dsw-label-caption)" }}
|
|
1294
1285
|
>
|
|
1295
1286
|
{line}
|