@rexvane/piweb 0.3.16 → 0.3.17
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 +8 -11
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +6 -6
- package/.next/react-loadable-manifest.json +4 -16
- package/.next/required-server-files.js +1 -3
- package/.next/required-server-files.json +1 -3
- package/.next/routes-manifest.json +0 -20
- 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 +4 -7
- package/.next/server/app/_not-found.segments/_full.segment.rsc +4 -7
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -5
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -5
- 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/git/route.js +2 -2
- package/.next/server/app/api/git/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/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/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 +3 -3
- 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/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 -9
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -7
- package/.next/server/app/index.segments/_full.segment.rsc +6 -9
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -5
- 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 +5 -8
- package/.next/server/app/login.segments/_full.segment.rsc +5 -8
- package/.next/server/app/login.segments/_tree.segment.rsc +2 -5
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +3 -6
- package/.next/server/app/page.js +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +8 -11
- package/.next/server/chunks/496.js +1 -1
- package/.next/server/chunks/532.js +1 -1
- package/.next/server/chunks/686.js +4 -8
- 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/server-reference-manifest.json +1 -1
- package/.next/static/chunks/2608.ec8ff1f5767c9056.js +1 -0
- package/.next/static/chunks/2716.93be8ad5c970c516.js +1 -0
- package/.next/static/chunks/4632.112d2e893d48d188.js +1 -0
- package/.next/static/chunks/4673.c1fb1580efc6ace7.js +1 -0
- package/.next/static/chunks/4686-f4a037d609cea3f2.js +1 -0
- package/.next/static/chunks/app/_global-error/page-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/events/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/agent/[id]/tree/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/agent/new/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/config/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/files/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/git/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/growth/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/health/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/models/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/pi-settings/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/plugins/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/prompts/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/security/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/export/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/sessions/[id]/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/sessions/import/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/sessions/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/skills/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/update/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/version/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/web-auth/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/api/workspaces/route-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/app/page-b51087831582ee37.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-f77e9ede9808b18e.js +1 -0
- package/.next/static/chunks/webpack-c31263fd83d75ce6.js +1 -0
- package/.next/static/css/850c2fd130ffa2fc.css +1 -0
- package/.next/static/zsynuO_9Kx6nrXgkTUJ04/_buildManifest.js +1 -0
- package/.next/types/routes.d.ts +1 -4
- package/.next/types/validator.ts +0 -27
- package/README.md +17 -29
- package/assets/archive.jpg +0 -0
- package/assets/growth-1.jpg +0 -0
- package/assets/growth-2.jpg +0 -0
- package/assets/import.jpg +0 -0
- package/assets/main.jpg +0 -0
- package/assets/outline.jpg +0 -0
- package/assets/prompts-1.jpg +0 -0
- package/assets/prompts-2.jpg +0 -0
- package/assets/providers-1.jpg +0 -0
- package/assets/providers-2.jpg +0 -0
- package/assets/providers-3.jpg +0 -0
- package/assets/trace-1.jpg +0 -0
- package/assets/trace-2.png +0 -0
- package/assets/trace-3.png +0 -0
- package/assets/trace-4.png +0 -0
- package/assets/viewer-1.jpg +0 -0
- package/assets/viewer-2.jpg +0 -0
- package/bin/pi.js +50 -0
- package/next.config.ts +32 -7
- package/package.json +4 -9
- package/scripts/launcher.mjs +1 -1
- package/scripts/pick-folder.ps1 +2 -11
- package/scripts/release.mjs +31 -11
- package/scripts/verify-package.mjs +0 -53
- package/src/app/api/config/route.ts +1 -4
- package/src/app/api/growth/route.ts +14 -14
- package/src/app/api/models/route.ts +2 -1
- package/src/app/api/security/route.ts +3 -8
- package/src/app/api/update/route.ts +2 -8
- package/src/app/globals.css +90 -777
- package/src/app/layout.tsx +6 -13
- package/src/components/AppShell.tsx +229 -521
- package/src/components/ChatInput.tsx +26 -153
- package/src/components/ChatWindow.tsx +231 -51
- package/src/components/ContextMeter.tsx +17 -11
- package/src/components/FileViewer.tsx +11 -10
- package/src/components/GitPanel.tsx +11 -13
- package/src/components/ModelSelector.tsx +6 -19
- package/src/components/PiMark.tsx +38 -5
- package/src/components/ProjectPanel.tsx +111 -95
- package/src/components/PromptPanel.tsx +51 -132
- package/src/components/SessionImportSection.tsx +37 -53
- package/src/components/SessionSidebar.tsx +57 -159
- package/src/components/SettingsPanel.tsx +193 -330
- package/src/components/icons.tsx +0 -23
- package/src/hooks/useGrowth.ts +123 -156
- package/src/hooks/usePiWeb.ts +15 -122
- package/src/i18n.tsx +42 -98
- package/src/lib/agent-manager.ts +21 -187
- package/src/lib/command-validation.ts +1 -9
- package/src/lib/growth-service.ts +390 -166
- package/src/lib/growth-tracker.ts +64 -264
- package/src/lib/growth-turns.ts +31 -1
- package/src/lib/models/model-scope.ts +15 -0
- package/src/lib/models-service.ts +16 -0
- package/src/lib/pi-settings.ts +7 -0
- package/src/lib/pi.ts +2 -3
- package/src/lib/session-import/claude.ts +2 -13
- package/src/lib/session-import/codex.ts +8 -44
- package/src/lib/session-import/files.ts +0 -14
- package/src/lib/session-import/grok.ts +24 -54
- package/src/lib/session-import/index.ts +0 -5
- package/src/lib/session-import/types.ts +0 -12
- package/src/lib/session-import/writer.ts +3 -8
- package/src/lib/skills-service.ts +9 -16
- package/src/lib/theme.ts +1 -68
- package/src/lib/trajectory.ts +0 -1
- package/src/lib/types.ts +27 -36
- package/tsconfig.json +2 -1
- package/.next/server/app/api/swarm/[id]/route.js +0 -1
- package/.next/server/app/api/swarm/[id]/route.js.nft.json +0 -1
- package/.next/server/app/api/swarm/[id]/route_client-reference-manifest.js +0 -1
- package/.next/server/app/api/swarm/route.js +0 -1
- package/.next/server/app/api/swarm/route.js.nft.json +0 -1
- package/.next/server/app/api/swarm/route_client-reference-manifest.js +0 -1
- package/.next/server/app/api/tests/route.js +0 -2
- package/.next/server/app/api/tests/route.js.nft.json +0 -1
- package/.next/server/app/api/tests/route_client-reference-manifest.js +0 -1
- package/.next/server/chunks/757.js +0 -1
- package/.next/server/chunks/775.js +0 -3
- package/.next/static/0t1jBUAuVaTFTXxRyIo3q/_buildManifest.js +0 -1
- package/.next/static/chunks/2608.43c58236c9cce8e6.js +0 -1
- package/.next/static/chunks/2716.4ec42df1d51354c9.js +0 -1
- package/.next/static/chunks/4632.62139b565b657aac.js +0 -1
- package/.next/static/chunks/4673.7796104395deb8d7.js +0 -1
- package/.next/static/chunks/4686-17f21a006739a596.js +0 -1
- package/.next/static/chunks/7577.10af9bb661652da8.js +0 -1
- package/.next/static/chunks/8866.38ec4ac4ee5a1b74.js +0 -1
- package/.next/static/chunks/app/_global-error/page-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/events/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/agent/[id]/tree/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/agent/new/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/config/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/files/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/git/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/growth/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/health/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/models/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/pi-settings/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/plugins/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/prompts/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/security/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/export/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/sessions/[id]/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/sessions/import/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/sessions/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/skills/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/swarm/[id]/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/swarm/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/tests/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/update/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/version/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/web-auth/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/api/workspaces/route-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/app/page-14379b8060d5cf07.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-5148173b3f55e415.js +0 -1
- package/.next/static/chunks/webpack-d1a69ad9edb38b11.js +0 -1
- package/.next/static/css/d0a0162cb481abe8.css +0 -1
- package/.next/types/app/api/swarm/[id]/route.ts +0 -353
- package/.next/types/app/api/swarm/route.ts +0 -353
- package/.next/types/app/api/tests/route.ts +0 -353
- package/assets/archive.png +0 -0
- package/assets/growth-1.png +0 -0
- package/assets/growth-2.png +0 -0
- package/assets/import.png +0 -0
- package/assets/main.png +0 -0
- package/assets/outline.png +0 -0
- package/assets/prompts-1.png +0 -0
- package/assets/prompts-2.png +0 -0
- package/assets/providers-1.png +0 -0
- package/assets/providers-2.png +0 -0
- package/assets/providers-3.png +0 -0
- package/assets/trace-1.png +0 -0
- package/assets/viewer-1.png +0 -0
- package/assets/viewer-2.png +0 -0
- package/scripts/managed-niubash-assets.mjs +0 -146
- package/scripts/prepare-niubash-packages.mjs +0 -182
- package/scripts/run-test-file.mjs +0 -34
- package/scripts/smoke-managed-niubash.mjs +0 -62
- package/scripts/verify-release-config.mjs +0 -24
- package/src/app/api/swarm/[id]/route.ts +0 -28
- package/src/app/api/swarm/route.ts +0 -24
- package/src/app/api/tests/route.ts +0 -27
- package/src/components/SwarmCoordinatorPod.tsx +0 -1021
- package/src/components/TestInspectorPod.tsx +0 -549
- package/src/lib/delegation.ts +0 -93
- package/src/lib/growth-rounds.ts +0 -84
- package/src/lib/managed-niubash.ts +0 -231
- package/src/lib/runtime-activity.ts +0 -27
- package/src/lib/session-import/lock.ts +0 -25
- package/src/lib/swarm-service.ts +0 -371
- package/src/lib/test-run-service.ts +0 -122
- package/src/lib/workflow.ts +0 -55
- /package/.next/static/{0t1jBUAuVaTFTXxRyIo3q → zsynuO_9Kx6nrXgkTUJ04}/_ssgManifest.js +0 -0
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
IconFileOutline16,
|
|
23
23
|
IconRefreshOutline14,
|
|
24
24
|
IconSearchOutline16,
|
|
25
|
-
IconSendArrowUp14,
|
|
26
25
|
IconTerminalOutline14,
|
|
27
26
|
IconThinkOutline14,
|
|
28
27
|
IconWarningOutline16,
|
|
@@ -77,13 +76,54 @@ function formatMessageClock(time: number, t: Dict): string {
|
|
|
77
76
|
return `${md} ${clock}`;
|
|
78
77
|
}
|
|
79
78
|
|
|
79
|
+
type MarkdownTextNode = { value?: unknown; children?: MarkdownTextNode[] };
|
|
80
|
+
function markdownNodeText(node: MarkdownTextNode | undefined): string {
|
|
81
|
+
if (!node) return "";
|
|
82
|
+
if (typeof node.value === "string") return node.value;
|
|
83
|
+
return node.children?.map(markdownNodeText).join("") ?? "";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function CopyButton({ text }: { text: string }) {
|
|
87
|
+
const [copied, setCopied] = useState(false);
|
|
88
|
+
const [copyFailed, setCopyFailed] = useState(false);
|
|
89
|
+
const { t } = useI18n();
|
|
90
|
+
return (
|
|
91
|
+
<>
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
className="icon-btn"
|
|
95
|
+
title={copyFailed ? t.copyFailed : t.copy}
|
|
96
|
+
aria-label={t.copy}
|
|
97
|
+
onClick={() => {
|
|
98
|
+
void copyText(text).then((ok) => {
|
|
99
|
+
setCopied(ok);
|
|
100
|
+
setCopyFailed(!ok);
|
|
101
|
+
setTimeout(() => { setCopied(false); setCopyFailed(false); }, 1200);
|
|
102
|
+
});
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
{copied ? <IconCheckOutline14 size={14} /> : <IconCopyOutline16 size={14} />}
|
|
106
|
+
</button>
|
|
107
|
+
{copyFailed && <span role="alert" style={{ fontSize: 11, color: "var(--dsw-danger)" }}>{t.copyFailed}</span>}
|
|
108
|
+
</>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
80
112
|
const Markdown = memo(function Markdown({ text }: { text: string }) {
|
|
81
113
|
return (
|
|
82
|
-
<div className="md
|
|
114
|
+
<div className="md">
|
|
83
115
|
<ReactMarkdown
|
|
84
116
|
remarkPlugins={[remarkGfm]}
|
|
85
117
|
rehypePlugins={[[rehypeHighlight, { detect: false, ignoreMissing: true }]]}
|
|
86
|
-
components={{
|
|
118
|
+
components={{
|
|
119
|
+
a: ({ node: _node, ...props }) => <a {...props} target="_blank" rel="noopener noreferrer" />,
|
|
120
|
+
pre: ({ node, children, ...props }) => (
|
|
121
|
+
<div className="md-code-block">
|
|
122
|
+
<div className="md-code-copy"><CopyButton text={markdownNodeText(node)} /></div>
|
|
123
|
+
<pre {...props}>{children}</pre>
|
|
124
|
+
</div>
|
|
125
|
+
),
|
|
126
|
+
}}
|
|
87
127
|
>
|
|
88
128
|
{text}
|
|
89
129
|
</ReactMarkdown>
|
|
@@ -888,8 +928,16 @@ const FinalAnswer = memo(function FinalAnswer({
|
|
|
888
928
|
);
|
|
889
929
|
});
|
|
890
930
|
|
|
931
|
+
/** 对话里提问下方的「本轮改了 N 个文件」:来自项目生长这一轮的 commit */
|
|
932
|
+
export interface RoundBadge {
|
|
933
|
+
commit: string;
|
|
934
|
+
files: number;
|
|
935
|
+
add: number;
|
|
936
|
+
del: number;
|
|
937
|
+
}
|
|
938
|
+
|
|
891
939
|
/** 行级 memo:流式期间只有最后一条消息变化,历史行全部跳过重渲染 */
|
|
892
|
-
const UserMessage = memo(function UserMessage({ message, onEditMessage }: { message: WebMessage; onEditMessage?: (entryId: string, text: string) => void }) {
|
|
940
|
+
const UserMessage = memo(function UserMessage({ message, badge, onShowRound, onEditMessage }: { message: WebMessage; badge?: RoundBadge; onShowRound?: (commit: string) => void; onEditMessage?: (entryId: string, text: string) => void }) {
|
|
893
941
|
const { t } = useI18n();
|
|
894
942
|
const text = message.content
|
|
895
943
|
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
@@ -898,77 +946,89 @@ const UserMessage = memo(function UserMessage({ message, onEditMessage }: { mess
|
|
|
898
946
|
const images = message.content.filter((content) => content.type === "image");
|
|
899
947
|
const [editing, setEditing] = useState(false);
|
|
900
948
|
const [draft, setDraft] = useState(text);
|
|
901
|
-
const [editorWidth, setEditorWidth] = useState(132);
|
|
902
|
-
const editRef = useRef<HTMLTextAreaElement>(null);
|
|
903
|
-
const bubbleRef = useRef<HTMLDivElement>(null);
|
|
904
949
|
const canEdit = Boolean(onEditMessage && message.id && text);
|
|
905
|
-
useEffect(() => {
|
|
906
|
-
if (!editing || !editRef.current) return;
|
|
907
|
-
editRef.current.style.height = "0px";
|
|
908
|
-
editRef.current.style.height = `${Math.min(320, editRef.current.scrollHeight)}px`;
|
|
909
|
-
}, [draft, editing]);
|
|
910
|
-
const imageStrip = images.length > 0 && (
|
|
911
|
-
<div className="mb-2 flex max-w-[85%] flex-wrap justify-end gap-2">
|
|
912
|
-
{images.map((image, index) => (
|
|
913
|
-
<img key={index} src={`data:${image.mimeType};base64,${image.data}`} alt="" className="max-h-72 max-w-full rounded-2xl object-contain" style={{ border: "0.5px solid var(--dsw-border-l2)" }} />
|
|
914
|
-
))}
|
|
915
|
-
</div>
|
|
916
|
-
);
|
|
917
|
-
function beginEdit() {
|
|
918
|
-
if (!canEdit) return;
|
|
919
|
-
setEditorWidth(Math.max(132, Math.ceil(bubbleRef.current?.getBoundingClientRect().width ?? 132)));
|
|
920
|
-
setDraft(text);
|
|
921
|
-
setEditing(true);
|
|
922
|
-
}
|
|
923
950
|
if (editing) {
|
|
924
951
|
return (
|
|
925
|
-
<div className="group mt-7 flex w-full flex-col items-end first:mt-0" data-role="user">
|
|
926
|
-
|
|
927
|
-
<div className="msg-user-bubble pw-user-editor" style={{ width: editorWidth }}>
|
|
952
|
+
<div className="group mt-7 flex w-full flex-col items-end first:mt-0" data-role="user" data-message-id={message.id}>
|
|
953
|
+
<div className="msg-user-bubble w-full">
|
|
928
954
|
<textarea
|
|
929
|
-
|
|
930
|
-
|
|
955
|
+
className="w-full resize-none bg-transparent outline-none"
|
|
956
|
+
style={{ minHeight: 68, font: "inherit", color: "inherit" }}
|
|
931
957
|
value={draft}
|
|
932
958
|
aria-label={t.editMessage}
|
|
933
959
|
autoFocus
|
|
934
960
|
onChange={(e) => setDraft(e.target.value)}
|
|
935
961
|
onKeyDown={(e) => {
|
|
936
962
|
if (e.key === "Escape") { e.preventDefault(); setEditing(false); }
|
|
937
|
-
if (e.key === "Enter" && !e.shiftKey
|
|
963
|
+
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); submit(); }
|
|
938
964
|
}}
|
|
939
965
|
/>
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
966
|
+
</div>
|
|
967
|
+
<div className="mt-1.5 flex items-center gap-2">
|
|
968
|
+
<button type="button" className="btn-outline" style={{ height: 26, padding: "0 10px", fontSize: 12 }} onClick={() => setEditing(false)}>
|
|
969
|
+
{t.cancel}
|
|
970
|
+
</button>
|
|
971
|
+
<button
|
|
972
|
+
type="button"
|
|
973
|
+
className="btn-primary-white"
|
|
974
|
+
style={{ height: 26, padding: "0 12px", fontSize: 12 }}
|
|
975
|
+
disabled={!draft.trim() || draft.trim() === text.trim()}
|
|
976
|
+
onClick={submit}
|
|
977
|
+
>
|
|
978
|
+
{t.sendEdit}
|
|
979
|
+
</button>
|
|
944
980
|
</div>
|
|
945
981
|
</div>
|
|
946
982
|
);
|
|
947
983
|
}
|
|
948
984
|
function submit() {
|
|
949
985
|
const next = draft.trim();
|
|
950
|
-
if (!next || !onEditMessage || !message.id) return;
|
|
986
|
+
if (!next || next === text.trim() || !onEditMessage || !message.id) return;
|
|
951
987
|
setEditing(false);
|
|
952
988
|
onEditMessage(message.id, next);
|
|
953
989
|
}
|
|
954
990
|
return (
|
|
955
991
|
// 回合边界:用户消息前留 28px(比回合内 8px 大得多),长对话里一眼找到“这一轮从哪开始”
|
|
956
|
-
<div className="group mt-7 flex w-full flex-col items-end first:mt-0" data-role="user">
|
|
957
|
-
{
|
|
992
|
+
<div className="group mt-7 flex w-full flex-col items-end first:mt-0" data-role="user" data-message-id={message.id}>
|
|
993
|
+
{images.length > 0 && (
|
|
994
|
+
<div className="mb-2 flex max-w-[85%] flex-wrap justify-end gap-2">
|
|
995
|
+
{images.map((image, index) => (
|
|
996
|
+
<img
|
|
997
|
+
key={index}
|
|
998
|
+
src={`data:${image.mimeType};base64,${image.data}`}
|
|
999
|
+
alt=""
|
|
1000
|
+
className="max-h-72 max-w-full rounded-2xl object-contain"
|
|
1001
|
+
style={{ border: "0.5px solid var(--dsw-border-l2)" }}
|
|
1002
|
+
/>
|
|
1003
|
+
))}
|
|
1004
|
+
</div>
|
|
1005
|
+
)}
|
|
958
1006
|
{/* 用户消息也走 Markdown:贴进来的代码块/列表不再是一坨纯文本 */}
|
|
959
|
-
{text && <div
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1007
|
+
{text && <div className="msg-user-bubble"><Markdown text={text} /></div>}
|
|
1008
|
+
{badge && (
|
|
1009
|
+
<button
|
|
1010
|
+
type="button"
|
|
1011
|
+
className="pw-chip mt-1.5"
|
|
1012
|
+
title={t.chatRoundShow}
|
|
1013
|
+
onClick={() => onShowRound?.(badge.commit)}
|
|
1014
|
+
data-testid="round-badge"
|
|
1015
|
+
>
|
|
1016
|
+
{badge.files ? t.chatRoundChanged.replace("{n}", String(badge.files)) : t.chatRoundNoChanges}
|
|
1017
|
+
{badge.files > 0 && (
|
|
1018
|
+
<>
|
|
1019
|
+
<span style={{ color: "var(--dsw-success)" }}> +{badge.add}</span>
|
|
1020
|
+
<span style={{ color: "var(--dsw-danger)" }}> −{badge.del}</span>
|
|
1021
|
+
</>
|
|
1022
|
+
)}
|
|
1023
|
+
</button>
|
|
1024
|
+
)}
|
|
965
1025
|
{/* 用户消息:复制 + 原地编辑重发(编辑后模型从这条消息重新回答);时钟在图标左侧(dsh clock=start) */}
|
|
966
1026
|
{text && (
|
|
967
1027
|
<MessageActions
|
|
968
1028
|
text={text}
|
|
969
1029
|
clockStart={message.timestamp !== undefined}
|
|
970
1030
|
time={message.timestamp}
|
|
971
|
-
onEdit={canEdit ?
|
|
1031
|
+
onEdit={canEdit ? () => { setDraft(text); setEditing(true); } : undefined}
|
|
972
1032
|
/>
|
|
973
1033
|
)}
|
|
974
1034
|
</div>
|
|
@@ -1021,6 +1081,8 @@ function TurnBlockView({
|
|
|
1021
1081
|
workingMessage,
|
|
1022
1082
|
onFork,
|
|
1023
1083
|
onEditMessage,
|
|
1084
|
+
roundBadge,
|
|
1085
|
+
onShowRound,
|
|
1024
1086
|
onInspectTool,
|
|
1025
1087
|
onOpenFile,
|
|
1026
1088
|
}: {
|
|
@@ -1040,6 +1102,8 @@ function TurnBlockView({
|
|
|
1040
1102
|
onFork?: (entryId: string) => void;
|
|
1041
1103
|
/** 编辑该回合的用户消息并重新发送(模型从这条消息重新回答) */
|
|
1042
1104
|
onEditMessage?: (entryId: string, text: string) => void;
|
|
1105
|
+
roundBadge?: RoundBadge;
|
|
1106
|
+
onShowRound?: (commit: string) => void;
|
|
1043
1107
|
onInspectTool?: (toolCallId: string) => void;
|
|
1044
1108
|
onOpenFile?: (path: string) => void;
|
|
1045
1109
|
}) {
|
|
@@ -1090,8 +1154,8 @@ function TurnBlockView({
|
|
|
1090
1154
|
const showProcess = items.length > 0 || Boolean(contextFiles?.length) || turnLive || (isLast && Boolean(retryNotice));
|
|
1091
1155
|
|
|
1092
1156
|
return (
|
|
1093
|
-
|
|
1094
|
-
{user && <UserMessage message={user} onEditMessage={onEditMessage} />}
|
|
1157
|
+
<>
|
|
1158
|
+
{user && <UserMessage message={user} badge={roundBadge} onShowRound={onShowRound} onEditMessage={onEditMessage} />}
|
|
1095
1159
|
{showProcess && (
|
|
1096
1160
|
<div className="pw-turn">
|
|
1097
1161
|
{contextFiles?.map((resource, i) => (
|
|
@@ -1121,7 +1185,7 @@ function TurnBlockView({
|
|
|
1121
1185
|
</div>
|
|
1122
1186
|
)}
|
|
1123
1187
|
{finalIdx >= 0 && <FinalAnswer message={messages[finalIdx]} onFork={onFork} showActions={!isStreaming || !isLast} turnStartMs={user?.timestamp} />}
|
|
1124
|
-
|
|
1188
|
+
</>
|
|
1125
1189
|
);
|
|
1126
1190
|
}
|
|
1127
1191
|
|
|
@@ -1206,6 +1270,11 @@ function turnBlockPropsEqual(prev: TurnBlockProps, next: TurnBlockProps): boolea
|
|
|
1206
1270
|
prev.contextFiles !== next.contextFiles ||
|
|
1207
1271
|
prev.onFork !== next.onFork ||
|
|
1208
1272
|
prev.onEditMessage !== next.onEditMessage ||
|
|
1273
|
+
prev.onShowRound !== next.onShowRound ||
|
|
1274
|
+
prev.roundBadge?.commit !== next.roundBadge?.commit ||
|
|
1275
|
+
prev.roundBadge?.files !== next.roundBadge?.files ||
|
|
1276
|
+
prev.roundBadge?.add !== next.roundBadge?.add ||
|
|
1277
|
+
prev.roundBadge?.del !== next.roundBadge?.del ||
|
|
1209
1278
|
prev.onInspectTool !== next.onInspectTool ||
|
|
1210
1279
|
prev.onOpenFile !== next.onOpenFile
|
|
1211
1280
|
) return false;
|
|
@@ -1228,6 +1297,28 @@ function turnBlockPropsEqual(prev: TurnBlockProps, next: TurnBlockProps): boolea
|
|
|
1228
1297
|
|
|
1229
1298
|
const TurnBlock = memo(TurnBlockView, turnBlockPropsEqual);
|
|
1230
1299
|
|
|
1300
|
+
const CHAT_WIDTH_KEY = "piweb.chatContentWidth";
|
|
1301
|
+
const CHAT_WIDTH_MIN = 480;
|
|
1302
|
+
const CHAT_WIDTH_MAX = 1200;
|
|
1303
|
+
|
|
1304
|
+
function resolveChatWidth(width: number, viewportWidth: number): number {
|
|
1305
|
+
const max = Math.max(320, Math.min(CHAT_WIDTH_MAX, viewportWidth - 64));
|
|
1306
|
+
return Math.max(Math.min(CHAT_WIDTH_MIN, max), Math.min(width, max));
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function setChatWidth(root: HTMLElement, width: number | null): void {
|
|
1310
|
+
const scope = root.closest<HTMLElement>(".pi-main");
|
|
1311
|
+
if (!scope) return;
|
|
1312
|
+
if (width === null) {
|
|
1313
|
+
scope.style.removeProperty("--dsh-chat-content-width");
|
|
1314
|
+
scope.style.removeProperty("--dsh-composer-card-max-width");
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
const resolved = resolveChatWidth(width, root.clientWidth);
|
|
1318
|
+
scope.style.setProperty("--dsh-chat-content-width", `${resolved}px`);
|
|
1319
|
+
scope.style.setProperty("--dsh-composer-card-max-width", `${resolved + 32}px`);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1231
1322
|
export function ChatWindow({
|
|
1232
1323
|
messages,
|
|
1233
1324
|
tools,
|
|
@@ -1246,6 +1337,8 @@ export function ChatWindow({
|
|
|
1246
1337
|
stats,
|
|
1247
1338
|
onOpenTrajectory,
|
|
1248
1339
|
onOpenFile,
|
|
1340
|
+
roundBadges,
|
|
1341
|
+
onShowRound,
|
|
1249
1342
|
}: {
|
|
1250
1343
|
messages: WebMessage[];
|
|
1251
1344
|
tools: Record<string, ToolCardState>;
|
|
@@ -1272,10 +1365,31 @@ export function ChatWindow({
|
|
|
1272
1365
|
/** 在本机编辑器打开工具行涉及的文件 */
|
|
1273
1366
|
onEditMessage?: (entryId: string, text: string) => void;
|
|
1274
1367
|
onOpenFile?: (path: string) => void;
|
|
1368
|
+
roundBadges?: ReadonlyMap<string, RoundBadge>;
|
|
1369
|
+
onShowRound?: (commit: string) => void;
|
|
1275
1370
|
}) {
|
|
1276
1371
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
1372
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
1373
|
+
const widthPreference = useRef<number | null>(null);
|
|
1374
|
+
const widthDrag = useRef<{ side: "left" | "right"; origin: number; width: number; pointerId: number } | null>(null);
|
|
1277
1375
|
const stickToBottom = useRef(true);
|
|
1278
1376
|
const { t } = useI18n();
|
|
1377
|
+
useEffect(() => {
|
|
1378
|
+
const root = rootRef.current;
|
|
1379
|
+
if (!root) return;
|
|
1380
|
+
const saved = Number(localStorage.getItem(CHAT_WIDTH_KEY));
|
|
1381
|
+
widthPreference.current = Number.isFinite(saved) && saved >= CHAT_WIDTH_MIN ? saved : null;
|
|
1382
|
+
const syncWidth = () => setChatWidth(root, window.innerWidth <= 840 ? null : widthPreference.current);
|
|
1383
|
+
const observer = new ResizeObserver(syncWidth);
|
|
1384
|
+
observer.observe(root);
|
|
1385
|
+
window.addEventListener("resize", syncWidth);
|
|
1386
|
+
syncWidth();
|
|
1387
|
+
return () => {
|
|
1388
|
+
observer.disconnect();
|
|
1389
|
+
window.removeEventListener("resize", syncWidth);
|
|
1390
|
+
setChatWidth(root, null);
|
|
1391
|
+
};
|
|
1392
|
+
}, []);
|
|
1279
1393
|
// 脱离底部后累计的新内容条数(回到底部按钮上的角标)
|
|
1280
1394
|
const [unseen, setUnseen] = useState(0);
|
|
1281
1395
|
/** 是否已离开底部(state 版:ref 变化不会触发渲染,静态会话翻页时按钮出不来) */
|
|
@@ -1321,10 +1435,74 @@ export function ChatWindow({
|
|
|
1321
1435
|
};
|
|
1322
1436
|
|
|
1323
1437
|
return (
|
|
1324
|
-
<div className="
|
|
1438
|
+
<div ref={rootRef} className="relative flex h-full min-h-0 flex-col">
|
|
1439
|
+
{(["left", "right"] as const).map((side) => (
|
|
1440
|
+
<div
|
|
1441
|
+
key={side}
|
|
1442
|
+
className="piweb-conversation-width-handle"
|
|
1443
|
+
data-side={side}
|
|
1444
|
+
role="separator"
|
|
1445
|
+
aria-orientation="vertical"
|
|
1446
|
+
aria-label={t.chatWidthHandle}
|
|
1447
|
+
tabIndex={0}
|
|
1448
|
+
onPointerDown={(event) => {
|
|
1449
|
+
if (event.button !== 0 || !rootRef.current) return;
|
|
1450
|
+
event.preventDefault();
|
|
1451
|
+
const width = widthPreference.current ?? Math.max(544, Math.min(rootRef.current.clientWidth * 0.64, 736));
|
|
1452
|
+
widthDrag.current = { side, origin: event.clientX, width, pointerId: event.pointerId };
|
|
1453
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
1454
|
+
event.currentTarget.dataset.dragging = "true";
|
|
1455
|
+
}}
|
|
1456
|
+
onPointerMove={(event) => {
|
|
1457
|
+
const drag = widthDrag.current;
|
|
1458
|
+
if (!drag || drag.pointerId !== event.pointerId || !rootRef.current) return;
|
|
1459
|
+
const delta = (event.clientX - drag.origin) * (side === "right" ? 2 : -2);
|
|
1460
|
+
drag.width = resolveChatWidth((widthPreference.current ?? drag.width) + delta, rootRef.current.clientWidth);
|
|
1461
|
+
drag.origin = event.clientX;
|
|
1462
|
+
widthPreference.current = drag.width;
|
|
1463
|
+
setChatWidth(rootRef.current, drag.width);
|
|
1464
|
+
}}
|
|
1465
|
+
onPointerUp={(event) => {
|
|
1466
|
+
const drag = widthDrag.current;
|
|
1467
|
+
if (!drag || drag.pointerId !== event.pointerId) return;
|
|
1468
|
+
widthDrag.current = null;
|
|
1469
|
+
event.currentTarget.removeAttribute("data-dragging");
|
|
1470
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
|
|
1471
|
+
if (rootRef.current) {
|
|
1472
|
+
const width = resolveChatWidth(drag.width, rootRef.current.clientWidth);
|
|
1473
|
+
widthPreference.current = width;
|
|
1474
|
+
localStorage.setItem(CHAT_WIDTH_KEY, String(width));
|
|
1475
|
+
setChatWidth(rootRef.current, width);
|
|
1476
|
+
}
|
|
1477
|
+
}}
|
|
1478
|
+
onPointerCancel={(event) => {
|
|
1479
|
+
event.currentTarget.removeAttribute("data-dragging");
|
|
1480
|
+
widthDrag.current = null;
|
|
1481
|
+
if (rootRef.current) setChatWidth(rootRef.current, widthPreference.current);
|
|
1482
|
+
}}
|
|
1483
|
+
onLostPointerCapture={(event) => {
|
|
1484
|
+
event.currentTarget.removeAttribute("data-dragging");
|
|
1485
|
+
if (widthDrag.current) {
|
|
1486
|
+
widthDrag.current = null;
|
|
1487
|
+
if (rootRef.current) setChatWidth(rootRef.current, widthPreference.current);
|
|
1488
|
+
}
|
|
1489
|
+
}}
|
|
1490
|
+
onWheel={(event) => scrollRef.current?.scrollBy({ top: event.deltaY })}
|
|
1491
|
+
onKeyDown={(event) => {
|
|
1492
|
+
if ((event.key === "ArrowLeft" || event.key === "ArrowRight") && rootRef.current) {
|
|
1493
|
+
event.preventDefault();
|
|
1494
|
+
const delta = event.key === "ArrowRight" ? 24 : -24;
|
|
1495
|
+
const width = resolveChatWidth((widthPreference.current ?? rootRef.current.clientWidth * 0.64) + delta, rootRef.current.clientWidth);
|
|
1496
|
+
widthPreference.current = width;
|
|
1497
|
+
localStorage.setItem(CHAT_WIDTH_KEY, String(width));
|
|
1498
|
+
setChatWidth(rootRef.current, width);
|
|
1499
|
+
}
|
|
1500
|
+
}}
|
|
1501
|
+
/>
|
|
1502
|
+
))}
|
|
1325
1503
|
<div
|
|
1326
1504
|
ref={scrollRef}
|
|
1327
|
-
className="
|
|
1505
|
+
className="min-h-0 flex-1 overflow-y-auto"
|
|
1328
1506
|
role="log"
|
|
1329
1507
|
aria-live="polite"
|
|
1330
1508
|
aria-relevant="additions"
|
|
@@ -1336,7 +1514,7 @@ export function ChatWindow({
|
|
|
1336
1514
|
if (atBottom) setUnseen(0);
|
|
1337
1515
|
}}
|
|
1338
1516
|
>
|
|
1339
|
-
<div className="
|
|
1517
|
+
<div className="mx-auto flex w-full flex-col px-4 py-6" style={{ maxWidth: "var(--dsh-chat-content-width)" }}>
|
|
1340
1518
|
{turns.map((turn, ti) => (
|
|
1341
1519
|
<TurnBlock
|
|
1342
1520
|
key={turn.key}
|
|
@@ -1355,6 +1533,8 @@ export function ChatWindow({
|
|
|
1355
1533
|
workingMessage={workingMessage}
|
|
1356
1534
|
onFork={onFork}
|
|
1357
1535
|
onEditMessage={onEditMessage}
|
|
1536
|
+
roundBadge={turn.userIndex >= 0 ? roundBadges?.get(messages[turn.userIndex].id ?? "") : undefined}
|
|
1537
|
+
onShowRound={onShowRound}
|
|
1358
1538
|
onInspectTool={onOpenTrajectory}
|
|
1359
1539
|
onOpenFile={onOpenFile}
|
|
1360
1540
|
/>
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* 上下文占用:
|
|
5
|
-
* 圆环常驻按钮 +
|
|
6
|
-
*
|
|
5
|
+
* 圆环常驻按钮 + 弹窗显示「百分比 / ~已用 / 窗口」与 13 类分段占用条
|
|
6
|
+
* (mac 磁盘存储条式的多色分段,含 Free Space 段)。
|
|
7
|
+
* 分段口径:全部为字符/4 启发式估算(与 SDK estimateTokens 一致),
|
|
7
8
|
* 服务端分项(System Prompt / Tools / Memory / Skills / Compacted / Buffer)来自快照,
|
|
8
9
|
* 消息分项(User / Agent Text / Thinking / Tool Call / Tool Output)弹窗打开时前端统计。
|
|
9
10
|
* 无对话时不渲染(由父组件经 contextVisible 控制)。
|
|
@@ -17,7 +18,7 @@ function fmtTok(n: number): string {
|
|
|
17
18
|
return n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(Math.round(n));
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
/**
|
|
21
|
+
/** 13 类分段的定义(顺序即渲染顺序) */
|
|
21
22
|
interface Segment {
|
|
22
23
|
key: string;
|
|
23
24
|
label: string;
|
|
@@ -90,6 +91,7 @@ export function ContextMeter({
|
|
|
90
91
|
{ key: "agentToolCall", label: t.contextAgentToolCall, tokens: messageSegments.agentToolCall, color: "#D3A15F" },
|
|
91
92
|
{ key: "toolOutput", label: t.contextToolOutput, tokens: messageSegments.toolOutput, color: "var(--dsw-warn)" },
|
|
92
93
|
{ key: "compacted", label: t.contextCompacted, tokens: b.compacted, color: "#A05A78" },
|
|
94
|
+
{ key: "buffer", label: t.contextAutoCompactBuffer, tokens: b.autoCompactBuffer, color: "var(--dsw-border-l3)" },
|
|
93
95
|
]
|
|
94
96
|
: // 服务端分项缺失(旧快照):退回旧三段口径
|
|
95
97
|
[
|
|
@@ -98,9 +100,11 @@ export function ContextMeter({
|
|
|
98
100
|
{ key: "tools", label: t.contextTools, tokens: Math.max(0, used - Math.max(0, Math.round((source?.systemChars ?? 0) / 4)) - messageSegments.user - messageSegments.agentText - messageSegments.agentThinking), color: "var(--dsw-warn)" },
|
|
99
101
|
];
|
|
100
102
|
|
|
101
|
-
//
|
|
102
|
-
const
|
|
103
|
-
const
|
|
103
|
+
// Free Space:窗口余量(窗口未知则不显示);分段条不归一化到已用之和
|
|
104
|
+
const usedSum = segments.reduce((sum, s) => sum + s.tokens, 0);
|
|
105
|
+
const free = window_ > 0 ? Math.max(0, window_ - usedSum) : 0;
|
|
106
|
+
const barTotal = window_ > 0 ? window_ : usedSum || 1;
|
|
107
|
+
const bar = (n: number) => `${(n / barTotal) * 100}%`;
|
|
104
108
|
|
|
105
109
|
return (
|
|
106
110
|
<div ref={ref} className="relative">
|
|
@@ -135,19 +139,21 @@ export function ContextMeter({
|
|
|
135
139
|
{tokens === null ? "—" : `~${fmtTok(used)}`} / {window_ > 0 ? fmtTok(window_) : "—"}
|
|
136
140
|
</div>
|
|
137
141
|
|
|
138
|
-
{
|
|
142
|
+
{/* 分段占用条(mac 磁盘存储条式多色分段,含 Free Space) */}
|
|
143
|
+
{usedSum > 0 && (
|
|
139
144
|
<div className="mt-3 flex h-2 w-full overflow-hidden rounded-full" style={{ background: "var(--dsw-selector)" }} aria-hidden>
|
|
140
|
-
|
|
145
|
+
{segments.filter((s) => s.tokens > 0).map((s) => (
|
|
146
|
+
<span key={s.key} style={{ width: bar(s.tokens), background: s.color }} />
|
|
147
|
+
))}
|
|
148
|
+
{free > 0 && <span style={{ width: bar(free), background: "transparent" }} />}
|
|
141
149
|
</div>
|
|
142
150
|
)}
|
|
143
|
-
<div className="mt-3" style={{ color: "var(--dsw-label-caption)", fontSize: 11 }}>{t.contextEstimateNote}</div>
|
|
144
151
|
<div className="mt-2 flex flex-col gap-1" style={{ color: "var(--dsw-label-tertiary)" }}>
|
|
145
152
|
{segments.filter((s) => s.tokens > 0).map((s) => (
|
|
146
153
|
<SegmentRow key={s.key} label={s.label} tokens={s.tokens} color={s.color} />
|
|
147
154
|
))}
|
|
155
|
+
{window_ > 0 && <SegmentRow label={t.contextFreeSpace} tokens={free} color="var(--dsw-selector)" />}
|
|
148
156
|
</div>
|
|
149
|
-
{b && b.autoCompactBuffer > 0 && <div className="mt-2 flex justify-between gap-2" style={{ color: "var(--dsw-label-tertiary)" }}><span>{t.contextAutoCompactBuffer}</span><span>{fmtTok(b.autoCompactBuffer)}</span></div>}
|
|
150
|
-
{window_ > 0 && <div className="mt-2 flex justify-between gap-2" style={{ color: "var(--dsw-label-secondary)" }}><span>{t.contextFreeSpace}</span><span>{free === null ? "—" : `~${fmtTok(free)}`}</span></div>}
|
|
151
157
|
</div>
|
|
152
158
|
)}
|
|
153
159
|
</div>
|
|
@@ -167,11 +167,12 @@ export function FileViewer({
|
|
|
167
167
|
} catch (error) {
|
|
168
168
|
if ((error as { status?: number }).status === 404) {
|
|
169
169
|
// 跟随中的文件可能在本轮后续工具里被删掉;从最近一次涉及它的快照取回最后内容。
|
|
170
|
-
const
|
|
171
|
-
|
|
170
|
+
const upto = growth.selected ? growth.rounds.findIndex((round) => round.commit === growth.selected!.commit) : growth.rounds.length - 1;
|
|
171
|
+
const prior = growth.rounds.slice(0, upto + 1).reverse().find((round) =>
|
|
172
|
+
round.changes.some((item) => item.path === tab.path || item.from === tab.path),
|
|
172
173
|
);
|
|
173
174
|
const priorTree = prior?.changes.some((item) => (item.status === "D" && item.path === tab.path) || (item.status === "R" && item.from === tab.path))
|
|
174
|
-
? prior.
|
|
175
|
+
? prior.parentTree
|
|
175
176
|
: prior?.tree;
|
|
176
177
|
if (priorTree && priorTree !== tree) {
|
|
177
178
|
try {
|
|
@@ -192,7 +193,7 @@ export function FileViewer({
|
|
|
192
193
|
try {
|
|
193
194
|
if (tab.lazy || !range) {
|
|
194
195
|
// 会话还没有任何快照时整棵树都是磁盘条目,「不在快照里」的提示会误导
|
|
195
|
-
await diskContent(tab.lazy && growth.
|
|
196
|
+
await diskContent(tab.lazy && growth.rounds.length > 0 ? "disk" : null);
|
|
196
197
|
return;
|
|
197
198
|
}
|
|
198
199
|
if (mode === "diff") {
|
|
@@ -223,7 +224,7 @@ export function FileViewer({
|
|
|
223
224
|
return () => {
|
|
224
225
|
alive = false;
|
|
225
226
|
};
|
|
226
|
-
}, [activeTab, cwd, mode, range, change, growth.
|
|
227
|
+
}, [activeTab, cwd, mode, range, change, growth.rounds, growth.selected, t.viewerLoadFailed]);
|
|
227
228
|
|
|
228
229
|
// 键盘:Esc 关(先关快速打开)、Ctrl/⌘+W 关页签、Ctrl/⌘+P 快速打开、[ ] 上一处下一处
|
|
229
230
|
useEffect(() => {
|
|
@@ -289,9 +290,9 @@ export function FileViewer({
|
|
|
289
290
|
setReveal({ path: dir, key: Date.now() });
|
|
290
291
|
onSelectTab(null);
|
|
291
292
|
};
|
|
292
|
-
const
|
|
293
|
-
const stepBadge =
|
|
294
|
-
? `${
|
|
293
|
+
const roundName = growth.selected ? (growth.selected.n ? t.growthRoundLabel.replace("{n}", String(growth.selected.n)) : t.growthKindUser) : "";
|
|
294
|
+
const stepBadge = roundName
|
|
295
|
+
? `${roundName}${change ? ` · +${change.add ?? 0} −${change.del ?? 0}` : ""}`
|
|
295
296
|
: "";
|
|
296
297
|
const isDiffMode = mode === "diff" && Boolean(loaded?.diff);
|
|
297
298
|
const noticeText = loaded?.notice === "deleted" ? t.viewerDeleted : loaded?.notice === "historical" ? t.viewerHistorical : loaded?.notice === "missing" ? t.viewerMissing : loaded?.notice === "disk" ? t.viewerNotInSnapshot : loaded?.notice === "nochange" ? t.viewerNoChanges : "";
|
|
@@ -463,10 +464,10 @@ export function FileViewer({
|
|
|
463
464
|
<div className="pw-viewer-toolbar hairline-b">
|
|
464
465
|
<div className="flex min-w-0 flex-1 items-center gap-1.5 rounded-lg px-2" style={{ background: "var(--dsw-hover)", height: 26, maxWidth: 360 }}>
|
|
465
466
|
<IconSearchOutline16 size={12} style={{ flex: "none", color: "var(--dsw-label-caption)" }} />
|
|
466
|
-
<input value={treeFilter} onChange={(e) => setTreeFilter(e.target.value)}
|
|
467
|
+
<input value={treeFilter} onChange={(e) => setTreeFilter(e.target.value)} placeholder={t.growthFilter} className="min-w-0 flex-1 bg-transparent" style={{ fontSize: 12 }} />
|
|
467
468
|
</div>
|
|
468
469
|
<span style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
|
|
469
|
-
{
|
|
470
|
+
{roundName}
|
|
470
471
|
</span>
|
|
471
472
|
<button type="button" className="pw-chip" onClick={growth.expandAll}>{t.growthExpandAll}</button>
|
|
472
473
|
<button type="button" className="pw-chip" onClick={growth.collapseAll}>{t.growthCollapseAll}</button>
|
|
@@ -181,29 +181,27 @@ export function GitPanel({
|
|
|
181
181
|
: t.gitPanel;
|
|
182
182
|
|
|
183
183
|
return (
|
|
184
|
-
<div className="flex h-full min-h-0 flex-col p-4
|
|
185
|
-
<div className="mb-3 flex items-center gap-2
|
|
184
|
+
<div className="flex h-full min-h-0 flex-col p-4">
|
|
185
|
+
<div className="mb-3 flex items-center gap-2">
|
|
186
186
|
{view ? (
|
|
187
|
-
<button className="
|
|
187
|
+
<button className="icon-btn" style={{ width: 22, height: 22 }} title={t.gitBack} onClick={() => changeView(null)}>
|
|
188
188
|
<IconChevronLeft14 size={13} />
|
|
189
189
|
</button>
|
|
190
190
|
) : (
|
|
191
|
-
<
|
|
192
|
-
<IconGitOutline16 size={15} />
|
|
193
|
-
</div>
|
|
191
|
+
<IconGitOutline16 size={15} style={{ flex: "none" }} />
|
|
194
192
|
)}
|
|
195
|
-
<span className="min-w-0 flex-1 truncate
|
|
193
|
+
<span className="min-w-0 flex-1 truncate" style={{ fontSize: 13, fontWeight: 600 }} title={view?.kind === "file" ? view.path : undefined}>
|
|
196
194
|
{headerTitle}
|
|
197
195
|
</span>
|
|
198
196
|
{view?.kind === "file" && onOpenFile && info?.root && (
|
|
199
|
-
<button className="
|
|
197
|
+
<button className="icon-btn" style={{ width: 22, height: 22 }} title={t.fileOpenEditor} onClick={() => onOpenFile(`${info.root}/${view.path}`)}>
|
|
200
198
|
<IconFileOutline16 size={13} />
|
|
201
199
|
</button>
|
|
202
200
|
)}
|
|
203
|
-
<button className="
|
|
204
|
-
<IconRefreshOutline14 size={13}
|
|
201
|
+
<button className="icon-btn" style={{ width: 22, height: 22 }} title={t.panelRefresh} onClick={() => (view ? void loadDiff(view) : void load())}>
|
|
202
|
+
<IconRefreshOutline14 size={13} />
|
|
205
203
|
</button>
|
|
206
|
-
<button className="
|
|
204
|
+
<button className="icon-btn" style={{ width: 22, height: 22 }} title={t.close} onClick={onClose}>
|
|
207
205
|
<IconCloseOutline14 size={13} />
|
|
208
206
|
</button>
|
|
209
207
|
</div>
|
|
@@ -216,7 +214,6 @@ export function GitPanel({
|
|
|
216
214
|
)}
|
|
217
215
|
|
|
218
216
|
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
219
|
-
{!cwd && <div className="rounded-xl px-3 py-4 text-sm" style={{ color: "var(--dsw-label-caption)", background: "var(--dsw-hover)" }}>请先在工作台选择工作区,再查看 Git 差异。</div>}
|
|
220
217
|
{/* ---------- 差异视图 ---------- */}
|
|
221
218
|
{view && (
|
|
222
219
|
<div>
|
|
@@ -270,7 +267,8 @@ export function GitPanel({
|
|
|
270
267
|
{!error && info.files.length > 0 && onAskCommit && (
|
|
271
268
|
<button
|
|
272
269
|
type="button"
|
|
273
|
-
className="
|
|
270
|
+
className="btn-outline self-start"
|
|
271
|
+
style={{ height: 28, padding: "0 12px", fontSize: 12 }}
|
|
274
272
|
onClick={onAskCommit}
|
|
275
273
|
title={t.gitAskCommitPrompt}
|
|
276
274
|
>
|