@rexvane/piweb 0.3.24 → 0.3.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +11 -11
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +5 -5
- package/.next/react-loadable-manifest.json +13 -20
- package/.next/required-server-files.js +2 -1
- package/.next/required-server-files.json +2 -1
- package/.next/routes-manifest.json +6 -8
- package/.next/server/app/_global-error/page.js +2 -2
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/agent/[id]/events/route.js +2 -2
- package/.next/server/app/api/agent/[id]/events/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/[id]/route.js +1 -1
- package/.next/server/app/api/agent/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
- package/.next/server/app/api/agent/[id]/tree/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/new/route.js +1 -1
- package/.next/server/app/api/agent/new/route.js.nft.json +1 -1
- package/.next/server/app/api/agent/resources/route.js +1 -0
- package/.next/server/app/api/agent/resources/route.js.nft.json +1 -0
- package/.next/server/app/api/agent/resources/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/config/route.js +1 -1
- package/.next/server/app/api/config/route.js.nft.json +1 -1
- package/.next/server/app/api/files/route.js +1 -1
- package/.next/server/app/api/files/route.js.nft.json +1 -1
- package/.next/server/app/api/fs/route.js +1 -1
- package/.next/server/app/api/fs/route.js.nft.json +1 -1
- package/.next/server/app/api/git/route.js +4 -4
- package/.next/server/app/api/git/route.js.nft.json +1 -1
- package/.next/server/app/api/github/route.js +4 -4
- package/.next/server/app/api/github/route.js.nft.json +1 -1
- package/.next/server/app/api/growth/route.js +1 -1
- package/.next/server/app/api/growth/route.js.nft.json +1 -1
- package/.next/server/app/api/health/route.js +1 -1
- package/.next/server/app/api/models/defaults/route.js +1 -1
- package/.next/server/app/api/models/defaults/route.js.nft.json +1 -1
- package/.next/server/app/api/models/route.js +1 -1
- package/.next/server/app/api/models/route.js.nft.json +1 -1
- package/.next/server/app/api/pi-settings/route.js +1 -1
- package/.next/server/app/api/pi-settings/route.js.nft.json +1 -1
- package/.next/server/app/api/plugins/route.js +1 -1
- package/.next/server/app/api/plugins/route.js.nft.json +1 -1
- package/.next/server/app/api/prompts/route.js +1 -1
- package/.next/server/app/api/prompts/route.js.nft.json +1 -1
- package/.next/server/app/api/remote/route.js +4 -3
- package/.next/server/app/api/security/route.js +1 -1
- package/.next/server/app/api/security/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/[id]/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/import/route.js +2 -2
- package/.next/server/app/api/sessions/import/route.js.nft.json +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/route.js +2 -2
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/update/route.js +2 -2
- package/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/server/app/api/version/route.js +1 -1
- package/.next/server/app/api/version/route.js.nft.json +1 -1
- package/.next/server/app/api/web-auth/route.js +2 -2
- package/.next/server/app/api/workspaces/route.js +1 -1
- package/.next/server/app/api/workspaces/route.js.nft.json +1 -1
- package/.next/server/app/icon.svg/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +6 -7
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +6 -7
- package/.next/server/app/index.segments/_full.segment.rsc +6 -7
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -3
- package/.next/server/app/login/page.js +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +1 -1
- package/.next/server/app/login.rsc +3 -3
- package/.next/server/app/login.segments/_full.segment.rsc +3 -3
- package/.next/server/app/login.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/page.js +1 -1
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +11 -11
- package/.next/server/chunks/3924.js +3 -3
- package/.next/server/chunks/538.js +1 -1
- package/.next/server/chunks/620.js +1 -0
- package/.next/server/chunks/6229.js +1 -1
- package/.next/server/chunks/7496.js +1 -1
- package/.next/server/chunks/7662.js +2 -2
- package/.next/server/chunks/8686.js +1 -1
- package/.next/server/functions-config-manifest.json +1 -2
- package/.next/server/instrumentation.js.nft.json +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/preview-props.json +3 -3
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/QJCtqgDKzGS2BdKABd9ch/_buildManifest.js +1 -0
- package/.next/static/chunks/2577.86c8538fbf05a8b2.js +1 -0
- package/.next/static/chunks/2716.fa561519675a3ff1.js +1 -0
- package/.next/static/chunks/2906.8a0e09bb47605cf1.js +1 -0
- package/.next/static/chunks/4673.ea36e55005c0f82f.js +1 -0
- package/.next/static/chunks/4686-4dc565817a160351.js +1 -0
- package/.next/static/chunks/4778.0d2c5d75b791d352.js +1 -0
- package/.next/static/chunks/{618.949981da99be7182.js → 618.9a22656b863f242b.js} +2 -2
- package/.next/static/chunks/app/_global-error/page-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/events/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/tree/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/new/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/agent/resources/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/config/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/files/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/fs/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/git/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/github/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/growth/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/health/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/models/defaults/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/models/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/pi-settings/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/plugins/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/prompts/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/remote/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/security/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/export/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/sessions/import/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/sessions/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/skills/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/update/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/version/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/web-auth/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/api/workspaces/route-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/app/page-71f2d3b3ed864b3d.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f2d93969c0bcf406.js +1 -0
- package/.next/static/chunks/webpack-ffc09b5fcc8aaa6d.js +1 -0
- package/.next/static/css/014eb1f21414e615.css +1 -0
- package/.next/static/css/56b129a13637fcfd.css +1 -0
- package/.next/static/css/7e3caf797e4cb19a.css +1 -0
- package/.next/types/app/api/agent/{[id]/side-chat → resources}/route.ts +3 -3
- package/.next/types/routes.d.ts +2 -2
- package/.next/types/validator.ts +9 -9
- package/README.md +1 -1
- package/next.config.ts +1 -1
- package/package.json +9 -9
- package/scripts/apply-global.mjs +87 -0
- package/scripts/launcher.mjs +21 -0
- package/scripts/testing/prepare-sdk-tools.mjs +19 -0
- package/src/app/api/agent/new/route.ts +2 -1
- package/src/app/api/agent/resources/route.ts +18 -0
- package/src/app/api/models/route.ts +22 -1
- package/src/app/api/plugins/route.ts +3 -2
- package/src/app/api/prompts/route.ts +3 -2
- package/src/app/api/remote/route.ts +22 -4
- package/src/app/api/sessions/[id]/export/route.ts +3 -1
- package/src/app/api/skills/route.ts +3 -2
- package/src/app/globals.css +1002 -1765
- package/src/components/AppShell.tsx +128 -93
- package/src/components/ChatInput.tsx +177 -98
- package/src/components/ChatWindow.tsx +189 -56
- package/src/components/ContextMeter.tsx +33 -9
- package/src/components/CustomSelect.module.css +142 -0
- package/src/components/CustomSelect.tsx +157 -0
- package/src/components/DrawerPanel.tsx +1 -1
- package/src/components/ExtensionUI.tsx +9 -3
- package/src/components/FileViewer.tsx +24 -21
- package/src/components/GitPanel.tsx +35 -13
- package/src/components/GrowthTree.tsx +6 -2
- package/src/components/ImageViewer.tsx +72 -0
- package/src/components/MarkdownBlock.tsx +67 -96
- package/src/components/ModelSelector.tsx +36 -39
- package/src/components/PiMark.tsx +76 -41
- package/src/components/PreviewImage.tsx +33 -0
- package/src/components/ProjectPanel.tsx +8 -5
- package/src/components/PromptPanel.tsx +85 -20
- package/src/components/ProviderSetupModal.module.css +12 -11
- package/src/components/ProviderSetupModal.tsx +107 -36
- package/src/components/RemoteSection.tsx +53 -2
- package/src/components/SessionImportSection.tsx +30 -15
- package/src/components/SessionSidebar.tsx +72 -62
- package/src/components/SettingsPanel.tsx +49 -151
- package/src/components/TrajectoryAnalyzer.module.css +26 -11
- package/src/components/TrajectoryView.tsx +11 -3
- package/src/components/chat/markdown-actions.tsx +23 -0
- package/src/components/composer/ComposerFiles.tsx +224 -0
- package/src/components/composer/ComposerImages.tsx +59 -127
- package/src/hooks/usePiWeb.ts +47 -2
- package/src/i18n.tsx +62 -80
- package/src/lib/agent-manager.ts +365 -33
- package/src/lib/chat/file-path.ts +54 -0
- package/src/lib/chat/skill-expansion.ts +51 -0
- package/src/lib/extension-ui.ts +25 -1
- package/src/lib/font.ts +37 -39
- package/src/lib/git-service.ts +4 -2
- package/src/lib/growth-turns.ts +3 -1
- package/src/lib/model-draft.ts +69 -2
- package/src/lib/models-service.ts +53 -3
- package/src/lib/pi.ts +2 -2
- package/src/lib/remote/assets.json +1 -1
- package/src/lib/remote/supervisor.ts +48 -5
- package/src/lib/security-service.ts +2 -1
- package/src/lib/session-reader.ts +25 -13
- package/src/lib/session-retention.ts +11 -8
- package/src/lib/skills-service.ts +2 -1
- package/src/lib/slash-command-token.ts +14 -0
- package/src/lib/trajectory.ts +111 -15
- package/src/lib/types.ts +28 -3
- package/src/lib/update-service.ts +29 -9
- package/src/lib/workspace-store.ts +36 -1
- package/.next/server/app/api/agent/[id]/side-chat/route.js +0 -5
- package/.next/server/app/api/agent/[id]/side-chat/route.js.nft.json +0 -1
- package/.next/server/app/api/agent/[id]/side-chat/route_client-reference-manifest.js +0 -1
- package/.next/server/chunks/3421.js +0 -1
- package/.next/static/chunks/2716.44fdf1b21454b961.js +0 -1
- package/.next/static/chunks/4673.e261707dce391be7.js +0 -1
- package/.next/static/chunks/4686-0cd87513f9ea3538.js +0 -1
- package/.next/static/chunks/4778.eddd3a5813f85824.js +0 -1
- package/.next/static/chunks/7411.5f79671208209b13.js +0 -1
- package/.next/static/chunks/8552.25c39ef4c995eba0.js +0 -1
- package/.next/static/chunks/app/_global-error/page-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/events/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/side-chat/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/tree/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/agent/new/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/config/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/files/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/fs/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/git/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/github/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/growth/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/health/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/models/defaults/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/models/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/pi-settings/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/plugins/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/prompts/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/remote/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/security/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/export/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/sessions/import/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/skills/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/update/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/version/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/web-auth/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/api/workspaces/route-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/app/page-dc3cb41d91c7432e.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-cbaaee0d7aacb85a.js +0 -1
- package/.next/static/chunks/webpack-2ac3d76b81895f5b.js +0 -1
- package/.next/static/css/08815507594e8b8d.css +0 -1
- package/.next/static/css/9fc559c2c4ad1d0f.css +0 -1
- package/.next/static/css/c6e2227c36ec2fd6.css +0 -1
- package/.next/static/css/f15a853af8b5127f.css +0 -1
- package/.next/static/lvKCJmjFj8LuqSojRVLRF/_buildManifest.js +0 -1
- package/src/app/api/agent/[id]/side-chat/route.ts +0 -77
- package/src/components/side-chat/SideChatPanel.module.css +0 -131
- package/src/components/side-chat/SideChatPanel.tsx +0 -189
- package/src/components/side-chat/icons.tsx +0 -8
- package/src/components/side-chat/request-history.ts +0 -29
- package/src/components/side-chat/store.ts +0 -248
- package/src/lib/chat/stream-settings.ts +0 -192
- package/src/lib/side-chat/context.ts +0 -123
- package/src/lib/side-chat/service.ts +0 -191
- package/src/lib/side-chat/types.ts +0 -61
- /package/.next/static/{lvKCJmjFj8LuqSojRVLRF → QJCtqgDKzGS2BdKABd9ch}/_ssgManifest.js +0 -0
|
@@ -20,15 +20,19 @@ import {
|
|
|
20
20
|
IconFileOutline16,
|
|
21
21
|
IconRefreshOutline14,
|
|
22
22
|
IconSearchOutline16,
|
|
23
|
+
IconSkillOutline16,
|
|
23
24
|
IconTerminalOutline14,
|
|
24
25
|
IconThinkOutline14,
|
|
25
26
|
IconWarningOutline16,
|
|
26
27
|
} from "@/components/icons";
|
|
27
28
|
import { DiffView, type DiffLine, parseUnifiedDiff } from "@/components/DiffView";
|
|
28
29
|
import { languageForPath } from "@/lib/highlight";
|
|
30
|
+
import { parseExpandedSkill } from "@/lib/chat/skill-expansion";
|
|
29
31
|
import { copyText } from "@/lib/clipboard";
|
|
30
32
|
import { OutlineRail } from "@/components/OutlineRail";
|
|
33
|
+
import { PreviewImage } from "@/components/PreviewImage";
|
|
31
34
|
import { ChatScrollContext, type ChatScroll } from "@/components/chat/chat-scroll";
|
|
35
|
+
import { MarkdownActionsProvider, type MarkdownActions } from "@/components/chat/markdown-actions";
|
|
32
36
|
import { useChatReadingAnchor } from "@/components/chat/reading-anchor";
|
|
33
37
|
import { useI18n } from "@/i18n";
|
|
34
38
|
import { fmtDuration, fmtPercent, fmtThroughput, fmtTokens } from "@/lib/format";
|
|
@@ -271,7 +275,7 @@ function fmtSpan(ms: number): string {
|
|
|
271
275
|
}
|
|
272
276
|
|
|
273
277
|
type StepTone = "dim" | "live" | "fail" | "warn";
|
|
274
|
-
type StepKind = ToolKind | "think" | "summary" | "context" | "error" | "abort" | "retry" | "work";
|
|
278
|
+
type StepKind = ToolKind | "think" | "summary" | "context" | "error" | "abort" | "retry" | "work" | "skill";
|
|
275
279
|
|
|
276
280
|
function KindIcon({ kind }: { kind: StepKind }) {
|
|
277
281
|
const p = { size: 14, style: { flex: "none" as const } };
|
|
@@ -297,6 +301,8 @@ function KindIcon({ kind }: { kind: StepKind }) {
|
|
|
297
301
|
return <IconRefreshOutline14 {...p} />;
|
|
298
302
|
case "work":
|
|
299
303
|
return <span className="pw-pi" aria-hidden>π</span>;
|
|
304
|
+
case "skill":
|
|
305
|
+
return <IconSkillOutline16 size={14} style={{ flex: "none" }} />;
|
|
300
306
|
default:
|
|
301
307
|
return <IconDataOutline16 {...p} />;
|
|
302
308
|
}
|
|
@@ -442,12 +448,20 @@ const ToolStep = memo(function ToolStep({
|
|
|
442
448
|
}, [args, isWriteTool, kind, output, state]);
|
|
443
449
|
const failed = Boolean(state?.isError) && !running;
|
|
444
450
|
const summary = toolSummary(name, args, cwd);
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
+
// 标题 JSON:先截断超长字符串值再序列化——write 的 content 几 MB,直接 stringify
|
|
452
|
+
// 每次渲染白算一遍再扔掉 99.9%;截后仍保 2000 字符上限的展示语义不变
|
|
453
|
+
const argsTitle = useMemo(() => {
|
|
454
|
+
const clip = (value: unknown): unknown =>
|
|
455
|
+
typeof value === "string" && value.length > 600 ? `${value.slice(0, 600)}…` : value;
|
|
456
|
+
try {
|
|
457
|
+
const shallow = args && typeof args === "object" && !Array.isArray(args)
|
|
458
|
+
? Object.fromEntries(Object.entries(args as Record<string, unknown>).map(([k, v]) => [k, clip(v)]))
|
|
459
|
+
: args;
|
|
460
|
+
return JSON.stringify(shallow, null, 2).slice(0, 2000);
|
|
461
|
+
} catch {
|
|
462
|
+
return summary.text;
|
|
463
|
+
}
|
|
464
|
+
}, [args, summary.text]);
|
|
451
465
|
const argPath = (() => {
|
|
452
466
|
if (kind !== "read" && kind !== "write") return "";
|
|
453
467
|
const a = args as { path?: unknown; file_path?: unknown } | undefined;
|
|
@@ -642,7 +656,7 @@ const ERROR_HINT_KEY: Record<ModelErrorKind, string> = {
|
|
|
642
656
|
moderation: "modelErrorModeration",
|
|
643
657
|
};
|
|
644
658
|
|
|
645
|
-
function ErrorStep({ message, count
|
|
659
|
+
function ErrorStep({ message, count }: { message: string; count: number }) {
|
|
646
660
|
const { t } = useI18n();
|
|
647
661
|
const tt = t as unknown as Dict;
|
|
648
662
|
const kind = classifyModelError(message);
|
|
@@ -655,7 +669,6 @@ function ErrorStep({ message, count, model }: { message: string; count: number;
|
|
|
655
669
|
{hint ? ` · ${hint}` : ""}
|
|
656
670
|
</span>
|
|
657
671
|
{count > 1 && <span className="pw-meta">{t.modelErrorTimes.replace("{n}", String(count))}</span>}
|
|
658
|
-
{model && <span className="pw-meta">{model}</span>}
|
|
659
672
|
</div>
|
|
660
673
|
</Step>
|
|
661
674
|
);
|
|
@@ -700,6 +713,41 @@ function ContextStep({ resource, cwd }: { resource: ContextResource | string; cw
|
|
|
700
713
|
);
|
|
701
714
|
}
|
|
702
715
|
|
|
716
|
+
// ---------- 技能调用:本轮用过哪个技能(不是一条用户发言)----------
|
|
717
|
+
/**
|
|
718
|
+
* pi 把 `/skill:name 参数` 在输入端展开成一段用户消息(SKILL.md 正文 + 参数接在 `</skill>` 后),
|
|
719
|
+
* 模型必须收到它。但那不是「用户发了一条消息」:
|
|
720
|
+
* - Claude Code 里技能是模型发起的 Skill 工具调用,用户打的正文是它的 `args` 入参
|
|
721
|
+
* (tool_result 只有 "Launching skill: name",不含 args);
|
|
722
|
+
* - Codex 里没有技能步骤,模型直接读 SKILL.md 文件。
|
|
723
|
+
* 所以这里不渲染用户气泡,而是放进本轮过程:一行「使用技能 name」,参数挂在这一行上、可展开看全文。
|
|
724
|
+
*/
|
|
725
|
+
function SkillStep({ name, args }: { name: string; args: string }) {
|
|
726
|
+
const { t } = useI18n();
|
|
727
|
+
const tt = t as unknown as Dict;
|
|
728
|
+
const [open, setOpen] = useState(false);
|
|
729
|
+
const oneLine = args.replace(/\s+/g, " ").trim();
|
|
730
|
+
const line = (
|
|
731
|
+
<>
|
|
732
|
+
<span className="pw-verb">{tt.skillStep}</span>
|
|
733
|
+
<span className="pw-skill-name">{name}</span>
|
|
734
|
+
{oneLine && !open && <span className="pw-gist">{oneLine}</span>}
|
|
735
|
+
</>
|
|
736
|
+
);
|
|
737
|
+
return (
|
|
738
|
+
<Step kind="skill">
|
|
739
|
+
{oneLine ? (
|
|
740
|
+
<button type="button" className="pw-line pw-clickable" aria-expanded={open} onClick={() => setOpen((value) => !value)} title={args}>
|
|
741
|
+
{line}
|
|
742
|
+
</button>
|
|
743
|
+
) : (
|
|
744
|
+
<div className="pw-line" title={`${tt.skillStepTitle} · ${name}`}>{line}</div>
|
|
745
|
+
)}
|
|
746
|
+
{open && oneLine && <pre className="pw-output pw-think-full">{args}</pre>}
|
|
747
|
+
</Step>
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
|
|
703
751
|
// ---------- 工作指示:π 记号 + 英文状态词闪光(没有正在进行的步骤时)或「Working for 12s」;整行固定英文 ----------
|
|
704
752
|
|
|
705
753
|
function WorkingStep({ startedAt, outputTokens, workingMessage, hasLiveStep }: { startedAt: number; outputTokens?: number; workingMessage?: string | null; hasLiveStep: boolean }) {
|
|
@@ -725,7 +773,7 @@ type Item =
|
|
|
725
773
|
| { kind: "narr"; key: string; msgIndex: number; text: string; live: boolean; error: boolean }
|
|
726
774
|
| { kind: "image"; key: string; msgIndex: number; data: string; mimeType: string }
|
|
727
775
|
| { kind: "tool"; key: string; msgIndex: number; call: ToolCallContent; state?: ToolCardState }
|
|
728
|
-
| { kind: "error"; key: string; msgIndex: number; message: string; count: number
|
|
776
|
+
| { kind: "error"; key: string; msgIndex: number; message: string; count: number }
|
|
729
777
|
| { kind: "abort"; key: string; msgIndex: number };
|
|
730
778
|
|
|
731
779
|
type Run = { kind: "run"; key: string; items: Item[]; hasThink: boolean; cmds: number; reads: number; searches: number; durationMs?: number };
|
|
@@ -743,7 +791,7 @@ function buildItems(messages: WebMessage[], k: number, tools: Record<string, Too
|
|
|
743
791
|
else if (c.type === "text" && !omitText && c.text.trim()) items.push({ kind: "narr", key, msgIndex: k, text: c.text, live: live && i === lastIndex, error: isError });
|
|
744
792
|
else if (c.type === "image" && !omitText) items.push({ kind: "image", key, msgIndex: k, data: c.data, mimeType: c.mimeType });
|
|
745
793
|
});
|
|
746
|
-
if (isError && (errorCount ?? 1) > 0) items.push({ kind: "error", key: `${k}-err`, msgIndex: k, message: message.errorMessage || message.stopReason || "error", count: errorCount ?? 1
|
|
794
|
+
if (isError && (errorCount ?? 1) > 0) items.push({ kind: "error", key: `${k}-err`, msgIndex: k, message: message.errorMessage || message.stopReason || "error", count: errorCount ?? 1 });
|
|
747
795
|
if (message.stopReason === "aborted") items.push({ kind: "abort", key: `${k}-abort`, msgIndex: k });
|
|
748
796
|
return items;
|
|
749
797
|
}
|
|
@@ -832,13 +880,13 @@ function ItemView({ item, cwd, onInspectTool, onOpenFile }: { item: Item; cwd?:
|
|
|
832
880
|
case "image":
|
|
833
881
|
return (
|
|
834
882
|
<div className="pw-narr">
|
|
835
|
-
<
|
|
883
|
+
<PreviewImage image={{ type: "image", mimeType: item.mimeType, data: item.data }} className="my-1 max-h-96 max-w-full rounded-2xl object-contain" />
|
|
836
884
|
</div>
|
|
837
885
|
);
|
|
838
886
|
case "tool":
|
|
839
887
|
return <ToolStep id={item.call.id} name={item.call.name} args={item.call.arguments} state={item.state} cwd={cwd} onInspectTool={onInspectTool} onOpenFile={onOpenFile} />;
|
|
840
888
|
case "error":
|
|
841
|
-
return <ErrorStep message={item.message} count={item.count}
|
|
889
|
+
return <ErrorStep message={item.message} count={item.count} />;
|
|
842
890
|
case "abort":
|
|
843
891
|
return <AbortStep />;
|
|
844
892
|
default:
|
|
@@ -869,26 +917,28 @@ const FinalAnswer = memo(function FinalAnswer({
|
|
|
869
917
|
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
870
918
|
.map((c) => c.text)
|
|
871
919
|
.join("");
|
|
920
|
+
const images = message.content.filter((c) => c.type === "image");
|
|
872
921
|
return (
|
|
873
922
|
<div className="group w-full" style={{ marginTop: 14 }} data-role="assistant">
|
|
874
923
|
{message.content.map((c, i) => {
|
|
875
924
|
if (c.type === "text" && c.text.trim()) return <Markdown key={i} text={c.text} />;
|
|
876
|
-
if (c.type === "image") return <
|
|
925
|
+
if (c.type === "image") return <PreviewImage key={i} image={c} images={images} index={images.indexOf(c)} className="my-2 max-h-96 max-w-full rounded-2xl object-contain" />;
|
|
877
926
|
return null;
|
|
878
927
|
})}
|
|
879
928
|
{showActions && (
|
|
880
|
-
<
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
929
|
+
<div>
|
|
930
|
+
<MessageActions
|
|
931
|
+
text={textAll.trim()}
|
|
932
|
+
onFork={message.id && onFork ? () => onFork(message.id!) : undefined}
|
|
933
|
+
durationMs={(message.endedAt ?? message.timestamp) != null && turnStartMs != null ? Math.max(0, (message.endedAt ?? message.timestamp)! - turnStartMs) : undefined}
|
|
934
|
+
usage={message.usage}
|
|
935
|
+
time={message.timestamp}
|
|
936
|
+
/>
|
|
937
|
+
</div>
|
|
887
938
|
)}
|
|
888
|
-
{/* 本轮改了 N
|
|
889
|
-
放在这里才读得通——先看到答案,再看到这一轮动了什么。 */}
|
|
939
|
+
{/* 本轮改了 N 个文件:独立换行,与上方复制按钮左边对齐 */}
|
|
890
940
|
{showActions && roundBadge && (
|
|
891
|
-
<div className="
|
|
941
|
+
<div className="mt-1.5 flex items-center">
|
|
892
942
|
<button
|
|
893
943
|
type="button"
|
|
894
944
|
className="pw-chip"
|
|
@@ -918,13 +968,37 @@ export interface RoundBadge {
|
|
|
918
968
|
del: number;
|
|
919
969
|
}
|
|
920
970
|
|
|
921
|
-
/**
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
const text = message.content
|
|
971
|
+
/** 用户消息里的纯文本(多个 text 块拼起来);技能识别与正常渲染都用它 */
|
|
972
|
+
function userTextOf(message: WebMessage): string {
|
|
973
|
+
return message.content
|
|
925
974
|
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
926
975
|
.map((c) => c.text)
|
|
927
976
|
.join("\n");
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/** 用户附带的图片;正常消息与技能消息共用一套排版(靠右、圆角、细描边) */
|
|
980
|
+
function UserImages({ images }: { images: Array<{ type: "image"; data: string; mimeType: string }> }) {
|
|
981
|
+
if (!images.length) return null;
|
|
982
|
+
return (
|
|
983
|
+
<div className="mb-2 flex max-w-[85%] flex-wrap justify-end gap-2 self-end">
|
|
984
|
+
{images.map((image, index) => (
|
|
985
|
+
<PreviewImage
|
|
986
|
+
key={index}
|
|
987
|
+
image={image}
|
|
988
|
+
images={images}
|
|
989
|
+
index={index}
|
|
990
|
+
className="max-h-72 max-w-full rounded-2xl object-contain"
|
|
991
|
+
style={{ border: "0.5px solid var(--dsw-border-l2)" }}
|
|
992
|
+
/>
|
|
993
|
+
))}
|
|
994
|
+
</div>
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/** 行级 memo:流式期间只有最后一条消息变化,历史行全部跳过重渲染 */
|
|
999
|
+
const UserMessage = memo(function UserMessage({ message, onEditMessage }: { message: WebMessage; onEditMessage?: (entryId: string, text: string) => void }) {
|
|
1000
|
+
const { t } = useI18n();
|
|
1001
|
+
const text = userTextOf(message);
|
|
928
1002
|
const images = message.content.filter((content) => content.type === "image");
|
|
929
1003
|
const [editing, setEditing] = useState(false);
|
|
930
1004
|
const [draft, setDraft] = useState(text);
|
|
@@ -932,6 +1006,8 @@ const UserMessage = memo(function UserMessage({ message, onEditMessage }: { mess
|
|
|
932
1006
|
if (editing) {
|
|
933
1007
|
return (
|
|
934
1008
|
<div className="group mt-7 flex w-full flex-col items-end first:mt-0" data-role="user" data-message-id={message.id}>
|
|
1009
|
+
{/* 编辑只改文字,图片由服务端从原消息里带进重发;照样显示,免得以为图被丢了 */}
|
|
1010
|
+
<UserImages images={images} />
|
|
935
1011
|
<div className="msg-user-bubble w-full">
|
|
936
1012
|
<textarea
|
|
937
1013
|
className="w-full resize-none bg-transparent outline-none"
|
|
@@ -954,7 +1030,7 @@ const UserMessage = memo(function UserMessage({ message, onEditMessage }: { mess
|
|
|
954
1030
|
type="button"
|
|
955
1031
|
className="btn-primary-white"
|
|
956
1032
|
style={{ height: 26, padding: "0 12px", fontSize: 12 }}
|
|
957
|
-
disabled={!draft.trim()
|
|
1033
|
+
disabled={!draft.trim()}
|
|
958
1034
|
onClick={submit}
|
|
959
1035
|
>
|
|
960
1036
|
{t.sendEdit}
|
|
@@ -965,27 +1041,18 @@ const UserMessage = memo(function UserMessage({ message, onEditMessage }: { mess
|
|
|
965
1041
|
}
|
|
966
1042
|
function submit() {
|
|
967
1043
|
const next = draft.trim();
|
|
968
|
-
|
|
1044
|
+
// 内容没改也允许重发:这个入口叫「取回编辑」,用户往往是想在同一位置重跑一遍,
|
|
1045
|
+
// 「内容没变」不等于「意图没变」。空内容仍然拦住(发空消息没有意义)。
|
|
1046
|
+
if (!next || !onEditMessage || !message.id) return;
|
|
969
1047
|
setEditing(false);
|
|
970
1048
|
onEditMessage(message.id, next);
|
|
971
1049
|
}
|
|
972
1050
|
return (
|
|
973
1051
|
// 回合边界:用户消息前留 28px(比回合内 8px 大得多),长对话里一眼找到“这一轮从哪开始”
|
|
974
1052
|
<div className="group mt-7 flex w-full flex-col items-end first:mt-0" data-role="user" data-message-id={message.id}>
|
|
975
|
-
{images
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
<img
|
|
979
|
-
key={index}
|
|
980
|
-
src={`data:${image.mimeType};base64,${image.data}`}
|
|
981
|
-
alt=""
|
|
982
|
-
className="max-h-72 max-w-full rounded-2xl object-contain"
|
|
983
|
-
style={{ border: "0.5px solid var(--dsw-border-l2)" }}
|
|
984
|
-
/>
|
|
985
|
-
))}
|
|
986
|
-
</div>
|
|
987
|
-
)}
|
|
988
|
-
{/* 用户消息也走 Markdown:贴进来的代码块/列表不再是一坨纯文本 */}
|
|
1053
|
+
<UserImages images={images} />
|
|
1054
|
+
{/* 用户消息也走 Markdown:贴进来的代码块/列表不再是一坨纯文本。
|
|
1055
|
+
技能展开的消息不会走到这里(在回合层被识别成技能步骤,见 TurnBlockView)。 */}
|
|
989
1056
|
{text && <div className="msg-user-bubble"><Markdown text={text} /></div>}
|
|
990
1057
|
{/* 用户消息:复制 + 原地编辑重发(编辑后模型从这条消息重新回答);时钟在图标左侧(dsh clock=start) */}
|
|
991
1058
|
{text && (
|
|
@@ -1002,7 +1069,8 @@ const UserMessage = memo(function UserMessage({ message, onEditMessage }: { mess
|
|
|
1002
1069
|
|
|
1003
1070
|
// ---------- 回合:用户消息 → 过程步骤 → 最终回答 ----------
|
|
1004
1071
|
|
|
1005
|
-
|
|
1072
|
+
/** summaryIndex:压缩分隔卡 / 分支摘要卡单独占一格,没有用户消息和过程 */
|
|
1073
|
+
type Turn = { key: string; userIndex: number; assistantIndexes: number[]; summaryIndex?: number };
|
|
1006
1074
|
|
|
1007
1075
|
function buildTurns(messages: WebMessage[]): Turn[] {
|
|
1008
1076
|
const out: Turn[] = [];
|
|
@@ -1011,6 +1079,10 @@ function buildTurns(messages: WebMessage[]): Turn[] {
|
|
|
1011
1079
|
if (m.role === "user") {
|
|
1012
1080
|
cur = { key: `u${i}`, userIndex: i, assistantIndexes: [] };
|
|
1013
1081
|
out.push(cur);
|
|
1082
|
+
} else if (m.role === "summary") {
|
|
1083
|
+
// 压缩可能切在回合中间:卡片后面的助手消息另起一个回合,不并回卡片上方的用户消息
|
|
1084
|
+
out.push({ key: `s${i}`, userIndex: -1, assistantIndexes: [], summaryIndex: i });
|
|
1085
|
+
cur = null;
|
|
1014
1086
|
} else if (m.role === "assistant") {
|
|
1015
1087
|
if (!cur) {
|
|
1016
1088
|
cur = { key: `a${i}`, userIndex: -1, assistantIndexes: [] };
|
|
@@ -1074,6 +1146,8 @@ function TurnBlockView({
|
|
|
1074
1146
|
}) {
|
|
1075
1147
|
const [expanded, setExpanded] = useState<Set<string>>(() => new Set());
|
|
1076
1148
|
const user = turn.userIndex >= 0 ? messages[turn.userIndex] : undefined;
|
|
1149
|
+
// 技能展开的用户消息不在这一层当「发言」渲染,改成本轮过程里的一个技能步骤
|
|
1150
|
+
const skill = useMemo(() => (user ? parseExpandedSkill(userTextOf(user)) : null), [user]);
|
|
1077
1151
|
const assistants = turn.assistantIndexes;
|
|
1078
1152
|
const lastIdx = assistants[assistants.length - 1];
|
|
1079
1153
|
const turnLive = isStreaming && isLast;
|
|
@@ -1116,13 +1190,15 @@ function TurnBlockView({
|
|
|
1116
1190
|
const blocks: Block[] = turnLive ? items.map((item) => ({ kind: "item", item })) : collapseRuns(items, finalIdx, spanOf);
|
|
1117
1191
|
const last = items[items.length - 1];
|
|
1118
1192
|
const hasLiveStep = Boolean(runningTool) || (last?.kind === "think" && last.live) || (last?.kind === "narr" && last.live);
|
|
1119
|
-
const showProcess = items.length > 0 || Boolean(contextFiles?.length) || turnLive || (isLast && Boolean(retryNotice));
|
|
1193
|
+
const showProcess = items.length > 0 || Boolean(contextFiles?.length) || turnLive || Boolean(skill) || (isLast && Boolean(retryNotice));
|
|
1120
1194
|
|
|
1121
1195
|
return (
|
|
1122
1196
|
<>
|
|
1123
|
-
{user && <UserMessage message={user} onEditMessage={onEditMessage} />}
|
|
1197
|
+
{user && !skill && <UserMessage message={user} onEditMessage={onEditMessage} />}
|
|
1198
|
+
{skill && <UserImages images={user?.content.filter((c) => c.type === "image") ?? []} />}
|
|
1124
1199
|
{showProcess && (
|
|
1125
1200
|
<div className="pw-turn">
|
|
1201
|
+
{skill && <SkillStep name={skill.name} args={skill.args} />}
|
|
1126
1202
|
{contextFiles?.map((resource, i) => (
|
|
1127
1203
|
<ContextStep key={`ctx-${typeof resource === "string" ? resource : `${resource.source}-${resource.path}`}-${i}`} resource={resource} cwd={cwd} />
|
|
1128
1204
|
))}
|
|
@@ -1245,6 +1321,45 @@ function TurnLite({ turn, messages }: { turn: Turn; messages: WebMessage[] }) {
|
|
|
1245
1321
|
);
|
|
1246
1322
|
}
|
|
1247
1323
|
|
|
1324
|
+
/**
|
|
1325
|
+
* 压缩分隔卡:卡片以上的消息照常显示(可编辑、可分叉),但模型只看得到它们的摘要;摘要可展开。
|
|
1326
|
+
* 分支摘要卡:切换分支时离开的那条分支被总结成摘要发给模型。
|
|
1327
|
+
*/
|
|
1328
|
+
function SummaryCard({ message }: { message: WebMessage }) {
|
|
1329
|
+
const { t } = useI18n();
|
|
1330
|
+
const [open, setOpen] = useState(false);
|
|
1331
|
+
const branch = message.summaryKind === "branch";
|
|
1332
|
+
const summary = message.summary?.trim();
|
|
1333
|
+
const title = branch
|
|
1334
|
+
? t.branchSummaryTitle
|
|
1335
|
+
: message.tokensBefore
|
|
1336
|
+
? t.compactedTitleTokens.replace("{tokens}", fmtTokens(message.tokensBefore))
|
|
1337
|
+
: t.compactedTitle;
|
|
1338
|
+
const rule = <span aria-hidden="true" style={{ flex: 1, borderTop: "1px dashed var(--dsw-border-l3)" }} />;
|
|
1339
|
+
return (
|
|
1340
|
+
<div className="w-full" data-role="summary" data-message-id={message.id} style={{ fontSize: "var(--piweb-chat-font-s)", color: "var(--dsw-label-caption)" }}>
|
|
1341
|
+
<div className="flex items-center gap-3">
|
|
1342
|
+
{rule}
|
|
1343
|
+
<span style={{ flex: "none", color: "var(--dsw-label-secondary)" }}>{title}</span>
|
|
1344
|
+
{rule}
|
|
1345
|
+
</div>
|
|
1346
|
+
<div className="mt-1 flex flex-wrap items-center justify-center gap-x-2 text-center">
|
|
1347
|
+
<span>{branch ? t.branchSummaryHint : t.compactedHint}</span>
|
|
1348
|
+
{summary && (
|
|
1349
|
+
<button type="button" className="hover:underline" aria-expanded={open} onClick={() => setOpen((value) => !value)} style={{ color: "var(--dsw-label-secondary)" }}>
|
|
1350
|
+
{open ? t.hideSummary : t.showSummary}
|
|
1351
|
+
</button>
|
|
1352
|
+
)}
|
|
1353
|
+
</div>
|
|
1354
|
+
{open && summary && (
|
|
1355
|
+
<div className="mt-2 rounded-xl px-3 py-2" style={{ background: "var(--dsw-hover)", color: "var(--dsw-label-primary)" }}>
|
|
1356
|
+
<Markdown text={summary} />
|
|
1357
|
+
</div>
|
|
1358
|
+
)}
|
|
1359
|
+
</div>
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1248
1363
|
export function SessionStatsBar({
|
|
1249
1364
|
stats,
|
|
1250
1365
|
}: {
|
|
@@ -1265,20 +1380,26 @@ export function SessionStatsBar({
|
|
|
1265
1380
|
if (stats.ttftSteps > 0) speeds.push(`${t.ttftAverage} ${fmtDuration(stats.ttftMs / stats.ttftSteps)}`);
|
|
1266
1381
|
if (stats.decodeMs > 0) speeds.push(`${fmtThroughput(stats.decodeTokens / (stats.decodeMs / 1000))} tok/s`);
|
|
1267
1382
|
if (speeds.length) groups.push(speeds.join(" · "));
|
|
1383
|
+
// 计费输入 = 新输入 + 命中缓存 + 写缓存。上游不返回 cacheRead/cacheWrite 时(很多中转与
|
|
1384
|
+
// 自建网关只给 prompt_tokens / completion_tokens),缓存命中率恒为 0——那不是「没命中」而是
|
|
1385
|
+
// 「没这个数据」,显示 0.00% 会误导,所以整项不显示。
|
|
1268
1386
|
const billedInput = stats.tokens.input + stats.tokens.cacheRead + stats.tokens.cacheWrite;
|
|
1387
|
+
const cacheReported = stats.tokens.cacheRead > 0 || stats.tokens.cacheWrite > 0;
|
|
1269
1388
|
if (billedInput > 0 || stats.tokens.output > 0) {
|
|
1270
|
-
if (billedInput > 0) groups.push(`${t.cacheHits} ${fmtPercent((stats.tokens.cacheRead / billedInput) * 100)}`);
|
|
1389
|
+
if (billedInput > 0 && cacheReported) groups.push(`${t.cacheHits} ${fmtPercent((stats.tokens.cacheRead / billedInput) * 100)}`);
|
|
1271
1390
|
groups.push(`${t.inputTokens} ${fmtTokens(billedInput)} tok · ${t.outputTokens} ${fmtTokens(stats.tokens.output)} tok`);
|
|
1272
1391
|
}
|
|
1273
1392
|
const line = groups.join(" | ");
|
|
1274
1393
|
return (
|
|
1275
|
-
//
|
|
1276
|
-
// 容器只有 544px
|
|
1277
|
-
//
|
|
1278
|
-
//
|
|
1394
|
+
// 悬停显示完整统计:自定义浮层固定在统计条上方(原生 title 位置不可控)。
|
|
1395
|
+
// 容器只有 544px(内容窄轴下限)而整行实测 959px,所以行本身保持单行 +
|
|
1396
|
+
// 省略号:统计条只有 20px 高,折行会把输入卡顶上去(手机上尤其明显)。
|
|
1397
|
+
// 想看完的方法有两条 —— 桌面悬停看浮层,手机上由「远程连接」的
|
|
1398
|
+
// mobile-fit.css 把这一行改成横向滑动(见 src/lib/remote/assets.json),
|
|
1399
|
+
// 所以 544px 里被省略号切掉的部分在手机上是滑得到的。
|
|
1279
1400
|
<div className="group relative mx-auto mt-1 block w-full px-4 pt-1 text-center">
|
|
1280
1401
|
<div
|
|
1281
|
-
className="
|
|
1402
|
+
className="overflow-hidden text-ellipsis whitespace-nowrap"
|
|
1282
1403
|
style={{ fontSize: "var(--dsh-content-font-size-secondary, 13px)", lineHeight: "20px", color: "var(--dsw-label-caption)" }}
|
|
1283
1404
|
>
|
|
1284
1405
|
{line}
|
|
@@ -1383,6 +1504,7 @@ export function ChatWindow({
|
|
|
1383
1504
|
stats,
|
|
1384
1505
|
onOpenTrajectory,
|
|
1385
1506
|
onOpenFile,
|
|
1507
|
+
onOpenPath,
|
|
1386
1508
|
roundBadges,
|
|
1387
1509
|
onShowRound,
|
|
1388
1510
|
}: {
|
|
@@ -1411,6 +1533,8 @@ export function ChatWindow({
|
|
|
1411
1533
|
/** 在本机编辑器打开工具行涉及的文件 */
|
|
1412
1534
|
onEditMessage?: (entryId: string, text: string) => void;
|
|
1413
1535
|
onOpenFile?: (path: string) => void;
|
|
1536
|
+
/** 正文行内代码里的路径:用页内文件查看器打开;没给就退回 onOpenFile(本机编辑器) */
|
|
1537
|
+
onOpenPath?: (path: string) => void;
|
|
1414
1538
|
roundBadges?: ReadonlyMap<string, RoundBadge>;
|
|
1415
1539
|
onShowRound?: (commit: string) => void;
|
|
1416
1540
|
}) {
|
|
@@ -1430,6 +1554,9 @@ export function ChatWindow({
|
|
|
1430
1554
|
},
|
|
1431
1555
|
}), []);
|
|
1432
1556
|
useChatReadingAnchor(rootRef, scrollRef, chatScroll.shouldPin);
|
|
1557
|
+
// 正文里可点击的文件路径(见 chat/markdown-actions):只认当前工作区内的相对路径,
|
|
1558
|
+
// 点开走页内文件查看器(不离开页面,路径不存在时查看器自己会报「文件不存在」)。
|
|
1559
|
+
const markdownActions = useMemo<MarkdownActions>(() => ({ openFile: onOpenPath ?? onOpenFile }), [onOpenPath, onOpenFile]);
|
|
1433
1560
|
const { t } = useI18n();
|
|
1434
1561
|
useEffect(() => {
|
|
1435
1562
|
const root = rootRef.current;
|
|
@@ -1467,6 +1594,8 @@ export function ChatWindow({
|
|
|
1467
1594
|
return -1;
|
|
1468
1595
|
}, [messages]);
|
|
1469
1596
|
const turns = useMemo(() => buildTurns(messages), [messages]);
|
|
1597
|
+
// 工作指示 / 重试行挂在最后一个真正的回合上,压缩卡追加在末尾时也不抢走
|
|
1598
|
+
const lastContentTurn = useMemo(() => turns.findLastIndex((turn) => turn.summaryIndex === undefined), [turns]);
|
|
1470
1599
|
const firstUser = useMemo(() => messages.findIndex((m) => m.role === "user"), [messages]);
|
|
1471
1600
|
|
|
1472
1601
|
// ---------- 回合懒渲染 ----------
|
|
@@ -1641,6 +1770,7 @@ export function ChatWindow({
|
|
|
1641
1770
|
/>
|
|
1642
1771
|
))}
|
|
1643
1772
|
<ChatScrollContext.Provider value={chatScroll}>
|
|
1773
|
+
<MarkdownActionsProvider value={markdownActions}>
|
|
1644
1774
|
<div
|
|
1645
1775
|
ref={scrollRef}
|
|
1646
1776
|
className="min-h-0 flex-1 overflow-y-auto"
|
|
@@ -1667,14 +1797,16 @@ export function ChatWindow({
|
|
|
1667
1797
|
{turns.map((turn, ti) => (
|
|
1668
1798
|
// 回合间 28px 的分隔挂在槽上:用户消息现在总是槽里的第一个子元素,它自己的 mt-7 会被 first:mt-0 吃掉
|
|
1669
1799
|
<div key={turn.key} data-turn-slot={ti} ref={slotRef(ti)} className="mt-7 first:mt-0">
|
|
1670
|
-
{
|
|
1800
|
+
{turn.summaryIndex !== undefined ? (
|
|
1801
|
+
<SummaryCard message={messages[turn.summaryIndex]} />
|
|
1802
|
+
) : isNear(ti) ? (
|
|
1671
1803
|
<TurnBlock
|
|
1672
1804
|
turn={turn}
|
|
1673
1805
|
messages={messages}
|
|
1674
1806
|
tools={tools}
|
|
1675
1807
|
cwd={cwd}
|
|
1676
1808
|
isStreaming={isStreaming}
|
|
1677
|
-
isLast={ti ===
|
|
1809
|
+
isLast={ti === lastContentTurn}
|
|
1678
1810
|
lastAssistantIndex={lastAssistantIndex}
|
|
1679
1811
|
contextFiles={turn.userIndex >= 0 && turn.userIndex === firstUser ? contextFiles : undefined}
|
|
1680
1812
|
retryNotice={retryNotice}
|
|
@@ -1695,7 +1827,7 @@ export function ChatWindow({
|
|
|
1695
1827
|
</div>
|
|
1696
1828
|
))}
|
|
1697
1829
|
{/* 还没有任何消息就已在流式(极少见):单独给一条工作指示 */}
|
|
1698
|
-
{isStreaming && streamStartedAt &&
|
|
1830
|
+
{isStreaming && streamStartedAt && lastContentTurn < 0 && (
|
|
1699
1831
|
<div className="pw-turn">
|
|
1700
1832
|
<WorkingStep startedAt={streamStartedAt} outputTokens={stats?.tokens.output} workingMessage={workingMessage} hasLiveStep={Boolean(runningTool)} />
|
|
1701
1833
|
</div>
|
|
@@ -1731,6 +1863,7 @@ export function ChatWindow({
|
|
|
1731
1863
|
)}
|
|
1732
1864
|
</div>
|
|
1733
1865
|
</div>
|
|
1866
|
+
</MarkdownActionsProvider>
|
|
1734
1867
|
</ChatScrollContext.Provider>
|
|
1735
1868
|
{/* 右侧大纲导航:从助手回答的标题和用户消息生成,随滚动高亮,悬停展开 */}
|
|
1736
1869
|
<OutlineRail scrollRef={scrollRef} revision={`${messages.length}:${nearTurns.size}:${isStreaming ? 1 : 0}`} />
|
|
@@ -8,17 +8,35 @@ import { formatCapacity, parseCapacity } from "@/lib/model-draft";
|
|
|
8
8
|
import { classifyMessageChars } from "@/lib/process-format";
|
|
9
9
|
import type { ContextBreakdown, TrajTokens } from "@/lib/types";
|
|
10
10
|
|
|
11
|
+
type ContextMessage = {
|
|
12
|
+
role?: string;
|
|
13
|
+
content: Array<{ type: string; text?: string; thinking?: string; arguments?: unknown; name?: string }>;
|
|
14
|
+
usage?: TrajTokens;
|
|
15
|
+
stopReason?: string;
|
|
16
|
+
timestamp?: number;
|
|
17
|
+
summaryKind?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 消息列表会照常显示压缩前的历史,但模型上下文只从最近一张压缩卡往后算(卡片以上只剩摘要,已计入 compacted)。
|
|
22
|
+
* 保留下来的旧助手消息用量是压缩前那次请求的,也不能当「最近一次输入」。
|
|
23
|
+
*/
|
|
24
|
+
function inContext(messages: ContextMessage[]): { messages: ContextMessage[]; since?: number } {
|
|
25
|
+
const at = messages.findLastIndex((m) => m.role === "summary" && m.summaryKind === "compaction");
|
|
26
|
+
return at < 0 ? { messages } : { messages: messages.slice(at + 1), since: messages[at].timestamp };
|
|
27
|
+
}
|
|
28
|
+
|
|
11
29
|
export function ContextMeter({ percent, tokens, contextWindow, model, source, label }: {
|
|
12
30
|
percent: number | null;
|
|
13
31
|
tokens: number | null;
|
|
14
32
|
contextWindow: number | null;
|
|
15
33
|
/** 当前会话的模型:有它才能在弹窗里改上下文窗口 */
|
|
16
34
|
model?: { provider: string; id: string } | null;
|
|
17
|
-
/**
|
|
35
|
+
/** 覆盖「上下文估算」这个措辞:拿到真实用量时不该叫估算 */
|
|
18
36
|
label?: string;
|
|
19
37
|
source?: {
|
|
20
38
|
systemChars: number;
|
|
21
|
-
messages:
|
|
39
|
+
messages: ContextMessage[];
|
|
22
40
|
breakdown?: ContextBreakdown;
|
|
23
41
|
loadFull?: () => Promise<unknown> | unknown;
|
|
24
42
|
};
|
|
@@ -38,9 +56,11 @@ export function ContextMeter({ percent, tokens, contextWindow, model, source, la
|
|
|
38
56
|
}, [open]);
|
|
39
57
|
|
|
40
58
|
// 流式期间只在打开弹窗后扫描,分类是实际文本字符数,不是假设的 token 数。
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
59
|
+
const context = useMemo(() => inContext(open ? source?.messages ?? [] : []), [open, source]);
|
|
60
|
+
const chars = useMemo(() => classifyMessageChars(context.messages), [context]);
|
|
61
|
+
const lastUsage = useMemo(() => context.messages.findLast((m) => m.role === "assistant" && m.stopReason !== "error" && m.stopReason !== "aborted" && m.stopReason !== "pending" &&
|
|
62
|
+
(context.since === undefined || (m.timestamp ?? 0) >= context.since) &&
|
|
63
|
+
(m.usage?.input ?? 0) + (m.usage?.cacheRead ?? 0) + (m.usage?.cacheWrite ?? 0) + (m.usage?.output ?? 0) > 0)?.usage, [context]);
|
|
44
64
|
const measuredInput = lastUsage ? (lastUsage.input ?? 0) + (lastUsage.cacheRead ?? 0) + (lastUsage.cacheWrite ?? 0) : null;
|
|
45
65
|
|
|
46
66
|
// 刚改过窗口:服务端已落盘,但页面上的 contextWindow 还是改之前的值(冷会话要等下一次快照)。
|
|
@@ -75,7 +95,6 @@ export function ContextMeter({ percent, tokens, contextWindow, model, source, la
|
|
|
75
95
|
{ key: "system", label: t.contextSystemPrompt, value: b?.systemPrompt, color: "var(--dsw-accent)" },
|
|
76
96
|
{ key: "builtin", label: t.contextSystemTools, value: b?.systemTools, color: "#8B7EC8" },
|
|
77
97
|
{ key: "custom", label: t.contextCustomTools, value: b?.customTools, color: "#B39DDB" },
|
|
78
|
-
{ key: "background", label: t.contextBackground, value: b?.background, color: "#5B8DEF" },
|
|
79
98
|
{ key: "memory", label: t.contextMemory, value: b?.memory, color: "#5FA8D3" },
|
|
80
99
|
{ key: "skills", label: t.contextSkills, value: b?.skills, color: "#4FB286" },
|
|
81
100
|
{ key: "user", label: t.contextUserMessages, value: chars.user, color: "var(--dsw-success)" },
|
|
@@ -100,15 +119,20 @@ export function ContextMeter({ percent, tokens, contextWindow, model, source, la
|
|
|
100
119
|
|
|
101
120
|
return (
|
|
102
121
|
<div ref={ref} className="relative">
|
|
103
|
-
<button className="icon-btn" style={{ width: 28, height: 28 }} title={usageLabel} aria-label={usageLabel} onClick={() => setOpen((o) => !o)}>
|
|
122
|
+
<button className="icon-btn isx-ring-btn" style={{ width: 28, height: 28 }} title={usageLabel} aria-label={usageLabel} onClick={() => setOpen((o) => !o)}>
|
|
104
123
|
<svg width="14" height="14" viewBox="0 0 14 14">
|
|
105
124
|
<circle cx="7" cy="7" r={r} fill="none" stroke="var(--dsw-border-l3)" strokeWidth="2" />
|
|
106
|
-
<circle cx="7" cy="7" r={r} fill="none" stroke={p > 85 ? "var(--dsw-warn)" : "var(--dsw-
|
|
125
|
+
<circle cx="7" cy="7" r={r} fill="none" stroke={p > 85 ? "var(--dsw-warn)" : "var(--dsw-label-primary)"} strokeWidth="2"
|
|
107
126
|
strokeDasharray={`${circumference * Math.max(0, Math.min(100, p)) / 100} ${circumference}`} transform="rotate(-90 7 7)" strokeLinecap="round" />
|
|
108
127
|
</svg>
|
|
109
128
|
</button>
|
|
110
129
|
{open && (
|
|
111
|
-
|
|
130
|
+
/* 弹窗右下角对齐在直径 28px 的圆环上,而圆环离屏幕右缘还有 60-70px:固定
|
|
131
|
+
w-72(288px) 在 ≤360px 的手机会往左顶出屏幕(实测 344px 下 left=-15)。
|
|
132
|
+
.pw-context-popover 不是给本仓 CSS 用的,是「远程连接」注入的 mobile-fit.css
|
|
133
|
+
认的钩子 —— 它用 calc(100vw-96px) + !important 压过那里的 .popover 兜底,重命名
|
|
134
|
+
或删掉这个类会让手机端重新冒出溢出。 */
|
|
135
|
+
<div className="pw-context-popover popover absolute bottom-9 right-0 z-50 w-72 max-w-[calc(100vw-96px)] p-4" style={{ fontSize: "var(--dsh-content-font-size-secondary)" }}>
|
|
112
136
|
{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>}
|
|
113
137
|
{window_ > 0 && (
|
|
114
138
|
<div className="mt-3 flex h-2 w-full overflow-hidden rounded-full" style={{ background: "var(--dsw-selector)" }} data-testid="context-bar" aria-hidden>
|