@rexvane/piweb 0.3.15 → 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 +4 -4
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/react-loadable-manifest.json +3 -3
- package/.next/required-server-files.js +1 -3
- package/.next/required-server-files.json +1 -3
- 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_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.nft.json +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.nft.json +1 -1
- package/.next/server/app/api/agent/new/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/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/plugins/route.js.nft.json +1 -1
- package/.next/server/app/api/prompts/route.js.nft.json +1 -1
- package/.next/server/app/api/security/route.js.nft.json +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.nft.json +1 -1
- package/.next/server/app/api/sessions/import/route.js +3 -3
- package/.next/server/app/api/sessions/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/update/route.js +1 -1
- 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/workspaces/route.js.nft.json +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +4 -4
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +4 -4
- package/.next/server/app/index.segments/_full.segment.rsc +4 -4
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- 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_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +4 -4
- package/.next/server/chunks/496.js +1 -1
- package/.next/server/chunks/686.js +4 -4
- 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/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/page-b51087831582ee37.js +1 -0
- package/.next/static/chunks/{webpack-42c5aae79b0e5d8c.js → webpack-c31263fd83d75ce6.js} +1 -1
- package/.next/static/css/850c2fd130ffa2fc.css +1 -0
- package/README.md +17 -19
- 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 +2 -6
- package/scripts/launcher.mjs +1 -1
- package/scripts/verify-package.mjs +0 -53
- package/src/app/api/growth/route.ts +14 -14
- package/src/app/api/models/route.ts +2 -1
- package/src/app/globals.css +90 -0
- package/src/components/AppShell.tsx +30 -16
- package/src/components/ChatWindow.tsx +192 -6
- package/src/components/FileViewer.tsx +10 -9
- package/src/components/ProjectPanel.tsx +111 -94
- package/src/components/SessionImportSection.tsx +20 -36
- package/src/hooks/useGrowth.ts +123 -156
- package/src/hooks/usePiWeb.ts +7 -10
- package/src/i18n.tsx +39 -39
- package/src/lib/agent-manager.ts +10 -8
- 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/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/types.ts +0 -12
- package/src/lib/types.ts +27 -22
- package/tsconfig.json +2 -1
- package/.next/static/chunks/2716.a9e808190a4e2424.js +0 -1
- package/.next/static/chunks/4632.0ed92ae42905ce40.js +0 -1
- package/.next/static/chunks/4673.d3d0a7f2599080fb.js +0 -1
- package/.next/static/chunks/4686-7d694def920ff093.js +0 -1
- package/.next/static/chunks/app/page-9a5440047c5a415c.js +0 -1
- package/.next/static/css/b2d35abc828d4713.css +0 -1
- 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/smoke-managed-niubash.mjs +0 -62
- package/src/lib/growth-rounds.ts +0 -84
- package/src/lib/managed-niubash.ts +0 -227
- /package/.next/static/{DdBwPYoHBKtaJ97QIr6Co → zsynuO_9Kx6nrXgkTUJ04}/_buildManifest.js +0 -0
- /package/.next/static/{DdBwPYoHBKtaJ97QIr6Co → zsynuO_9Kx6nrXgkTUJ04}/_ssgManifest.js +0 -0
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
CustomProvidersValidationError,
|
|
8
8
|
discoverModels,
|
|
9
9
|
listModels,
|
|
10
|
+
listModelsForPiWeb,
|
|
10
11
|
loginState,
|
|
11
12
|
providerUsage,
|
|
12
13
|
readCustomProviders,
|
|
@@ -37,7 +38,7 @@ export async function GET(req: Request) {
|
|
|
37
38
|
const full = custom || params.get("full") === "1";
|
|
38
39
|
try {
|
|
39
40
|
const customProviders = custom ? await readCustomProviders() : null;
|
|
40
|
-
const data = await listModels();
|
|
41
|
+
const data = await (full ? listModels() : listModelsForPiWeb());
|
|
41
42
|
// 页面挂载只需要选模型用的字段(1300+ 个模型带 api/baseUrl/cost/input 有 400KB);设置页用 custom=1 拿完整版
|
|
42
43
|
const models = full
|
|
43
44
|
? data.models
|
package/src/app/globals.css
CHANGED
|
@@ -221,11 +221,47 @@ textarea::placeholder {
|
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
.piweb-conversation-width-handle {
|
|
225
|
+
position: absolute;
|
|
226
|
+
top: 0;
|
|
227
|
+
bottom: 0;
|
|
228
|
+
z-index: 3;
|
|
229
|
+
width: 12px;
|
|
230
|
+
cursor: col-resize;
|
|
231
|
+
touch-action: none;
|
|
232
|
+
}
|
|
233
|
+
.piweb-conversation-width-handle[data-side="left"] {
|
|
234
|
+
left: calc(50% - var(--dsh-chat-content-width) / 2 - 20px);
|
|
235
|
+
}
|
|
236
|
+
.piweb-conversation-width-handle[data-side="right"] {
|
|
237
|
+
right: calc(50% - var(--dsh-chat-content-width) / 2 - 20px);
|
|
238
|
+
}
|
|
239
|
+
.piweb-conversation-width-handle::after {
|
|
240
|
+
content: "";
|
|
241
|
+
position: absolute;
|
|
242
|
+
top: 0;
|
|
243
|
+
bottom: 0;
|
|
244
|
+
left: 5px;
|
|
245
|
+
width: 2px;
|
|
246
|
+
border-radius: 2px;
|
|
247
|
+
background: var(--dsw-border-l3);
|
|
248
|
+
opacity: 0;
|
|
249
|
+
transition: opacity var(--ds-duration-fast);
|
|
250
|
+
}
|
|
251
|
+
.piweb-conversation-width-handle:hover::after,
|
|
252
|
+
.piweb-conversation-width-handle[data-dragging]::after,
|
|
253
|
+
.piweb-conversation-width-handle:focus-visible::after {
|
|
254
|
+
opacity: 1;
|
|
255
|
+
}
|
|
256
|
+
|
|
224
257
|
@media (max-width: 840px) {
|
|
225
258
|
:root {
|
|
226
259
|
--dsh-chat-content-width: 100%;
|
|
227
260
|
--dsh-composer-card-max-width: 100%;
|
|
228
261
|
}
|
|
262
|
+
.piweb-conversation-width-handle {
|
|
263
|
+
display: none;
|
|
264
|
+
}
|
|
229
265
|
|
|
230
266
|
.pi-main > .hairline-b {
|
|
231
267
|
padding-left: 56px;
|
|
@@ -739,6 +775,18 @@ textarea::placeholder {
|
|
|
739
775
|
font-size: 13px;
|
|
740
776
|
line-height: 1.55;
|
|
741
777
|
}
|
|
778
|
+
.md-code-block {
|
|
779
|
+
position: relative;
|
|
780
|
+
}
|
|
781
|
+
.md-code-copy {
|
|
782
|
+
position: absolute;
|
|
783
|
+
top: 12px;
|
|
784
|
+
right: 12px;
|
|
785
|
+
z-index: 1;
|
|
786
|
+
}
|
|
787
|
+
.md-code-block pre {
|
|
788
|
+
padding-top: 40px;
|
|
789
|
+
}
|
|
742
790
|
.md table {
|
|
743
791
|
border-collapse: collapse;
|
|
744
792
|
margin: 0.7em 0;
|
|
@@ -1769,6 +1817,48 @@ body[data-pebrel-theme="pebrel"][data-ds-dark-theme] {
|
|
|
1769
1817
|
.pw-timeline {
|
|
1770
1818
|
flex: none;
|
|
1771
1819
|
}
|
|
1820
|
+
/* 生长时间轴:每轮一根柱,底对齐,超出横向滚动;高度由内联样式给(= 改动行数) */
|
|
1821
|
+
.pw-tl-rail {
|
|
1822
|
+
display: flex;
|
|
1823
|
+
align-items: flex-end;
|
|
1824
|
+
gap: 3px;
|
|
1825
|
+
height: 34px;
|
|
1826
|
+
padding: 6px 10px 2px;
|
|
1827
|
+
overflow-x: auto;
|
|
1828
|
+
scrollbar-width: none;
|
|
1829
|
+
}
|
|
1830
|
+
.pw-tl-rail::-webkit-scrollbar {
|
|
1831
|
+
display: none;
|
|
1832
|
+
}
|
|
1833
|
+
.pw-tl-bar {
|
|
1834
|
+
flex: none;
|
|
1835
|
+
width: 8px;
|
|
1836
|
+
min-height: 3px;
|
|
1837
|
+
border-radius: 2px 2px 1px 1px;
|
|
1838
|
+
opacity: 0.5;
|
|
1839
|
+
transition: opacity var(--ds-duration-fast) var(--ds-ease-in-out);
|
|
1840
|
+
}
|
|
1841
|
+
.pw-tl-bar:hover {
|
|
1842
|
+
opacity: 0.85;
|
|
1843
|
+
}
|
|
1844
|
+
.pw-tl-bar[data-active] {
|
|
1845
|
+
opacity: 1;
|
|
1846
|
+
box-shadow: 0 0 0 1px var(--dsw-sidebar-fill), 0 0 0 2px var(--dsw-label-secondary);
|
|
1847
|
+
}
|
|
1848
|
+
.pw-tl-bar[data-kind="user"] {
|
|
1849
|
+
width: 4px;
|
|
1850
|
+
}
|
|
1851
|
+
.pw-tl-bar[data-running] {
|
|
1852
|
+
height: 14px;
|
|
1853
|
+
border: 1px dashed var(--dsw-label-caption);
|
|
1854
|
+
background: transparent;
|
|
1855
|
+
animation: pw-tl-breathe 1.6s var(--ds-ease-in-out) infinite;
|
|
1856
|
+
}
|
|
1857
|
+
@keyframes pw-tl-breathe {
|
|
1858
|
+
50% {
|
|
1859
|
+
opacity: 0.2;
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1772
1862
|
|
|
1773
1863
|
/* 文件查看器:80vw × 80vh 居中,全屏切 100% */
|
|
1774
1864
|
.pw-viewer {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
8
8
|
import { PiMark } from "@/components/PiMark";
|
|
9
9
|
import { ChatInput, EMPTY_CHAT_DRAFT, type ChatDraft, type ChatDraftUpdate } from "@/components/ChatInput";
|
|
10
|
-
import { ChatWindow, SessionStatsBar } from "@/components/ChatWindow";
|
|
10
|
+
import { ChatWindow, SessionStatsBar, type RoundBadge } from "@/components/ChatWindow";
|
|
11
11
|
import { ExtensionDialogHost, ExtensionNotices } from "@/components/ExtensionUI";
|
|
12
12
|
import dynamic from "next/dynamic";
|
|
13
13
|
import { SessionSidebar } from "@/components/SessionSidebar";
|
|
@@ -574,29 +574,39 @@ export function AppShell() {
|
|
|
574
574
|
useEffect(() => {
|
|
575
575
|
resetViewer();
|
|
576
576
|
}, [panelCwd, resetViewer]);
|
|
577
|
-
const growthTurnStarts = useMemo(
|
|
578
|
-
() => {
|
|
579
|
-
const persisted = state.snapshot?.userTurns ?? [];
|
|
580
|
-
const ids = new Set(persisted.map((turn) => turn.id));
|
|
581
|
-
const lastTs = persisted[persisted.length - 1]?.ts ?? -Infinity;
|
|
582
|
-
const live = state.messages
|
|
583
|
-
.filter((message) => message.role === "user" && (!message.id || !ids.has(message.id)) && (message.timestamp ?? Date.now()) >= lastTs - 1000)
|
|
584
|
-
.map((message) => message.timestamp ?? Date.now());
|
|
585
|
-
return [...persisted.map((turn) => turn.ts), ...live];
|
|
586
|
-
},
|
|
587
|
-
[state.snapshot?.userTurns, state.messages],
|
|
588
|
-
);
|
|
589
577
|
const growth = useGrowth({
|
|
590
578
|
cwd: panelCwd,
|
|
591
579
|
sessionPath: currentPath,
|
|
592
|
-
|
|
593
|
-
pending: state.growth.pending,
|
|
580
|
+
liveRounds: state.growth.rounds,
|
|
594
581
|
runtimeError: state.growth.error,
|
|
595
582
|
active: projectOpen || viewer.state.open,
|
|
583
|
+
watchRounds: Boolean(currentPath),
|
|
596
584
|
connected: state.connected,
|
|
597
|
-
turnStarts: growthTurnStarts,
|
|
598
585
|
});
|
|
599
586
|
const openFileNode = useCallback((node: TreeNode) => viewer.open(node.path, { from: node.from, lazy: node.lazy }), [viewer]);
|
|
587
|
+
const roundBadges = useMemo(() => {
|
|
588
|
+
const map = new Map<string, RoundBadge>();
|
|
589
|
+
for (const round of growth.rounds) {
|
|
590
|
+
if (round.kind !== "round" || !round.promptIds[0]) continue;
|
|
591
|
+
const st = round.stats;
|
|
592
|
+
map.set(round.promptIds[0], { commit: round.commit, files: st.added + st.modified + st.deleted + st.renamed, add: st.add, del: st.del });
|
|
593
|
+
}
|
|
594
|
+
return map;
|
|
595
|
+
}, [growth.rounds]);
|
|
596
|
+
const growthSelect = growth.select;
|
|
597
|
+
const showRound = useCallback((commit: string) => {
|
|
598
|
+
toggleProject(true);
|
|
599
|
+
growthSelect(commit);
|
|
600
|
+
}, [toggleProject, growthSelect]);
|
|
601
|
+
const jumpToChat = useCallback((messageId: string) => {
|
|
602
|
+
requestAnimationFrame(() => {
|
|
603
|
+
const node = document.querySelector<HTMLElement>(`[data-role="user"][data-message-id="${CSS.escape(messageId)}"]`);
|
|
604
|
+
if (!node) return;
|
|
605
|
+
node.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
606
|
+
node.dataset.flash = "";
|
|
607
|
+
setTimeout(() => delete node.dataset.flash, 1600);
|
|
608
|
+
});
|
|
609
|
+
}, []);
|
|
600
610
|
|
|
601
611
|
useEffect(() => {
|
|
602
612
|
document.title = currentId && title && title !== "pi" ? `${title} · pi` : "pi";
|
|
@@ -622,6 +632,8 @@ export function AppShell() {
|
|
|
622
632
|
workspaceName={panelCwd ? getWorkspaceName(panelCwd) : ""}
|
|
623
633
|
cwd={panelCwd}
|
|
624
634
|
hasSession={Boolean(currentPath)}
|
|
635
|
+
running={isStreaming}
|
|
636
|
+
onJumpToChat={jumpToChat}
|
|
625
637
|
onOpenFile={openFileNode}
|
|
626
638
|
onReference={insertIntoComposer}
|
|
627
639
|
onOpenEditor={openInEditor}
|
|
@@ -845,6 +857,8 @@ export function AppShell() {
|
|
|
845
857
|
onOpenFile={openInEditor}
|
|
846
858
|
onFork={handleFork}
|
|
847
859
|
onEditMessage={doEditMessage}
|
|
860
|
+
roundBadges={roundBadges}
|
|
861
|
+
onShowRound={showRound}
|
|
848
862
|
/>
|
|
849
863
|
<div className="px-4 pb-3 pt-2">
|
|
850
864
|
<div className="mx-auto w-full" style={{ maxWidth: "var(--dsh-composer-card-max-width)" }}>
|
|
@@ -76,13 +76,54 @@ function formatMessageClock(time: number, t: Dict): string {
|
|
|
76
76
|
return `${md} ${clock}`;
|
|
77
77
|
}
|
|
78
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
|
+
|
|
79
112
|
const Markdown = memo(function Markdown({ text }: { text: string }) {
|
|
80
113
|
return (
|
|
81
114
|
<div className="md">
|
|
82
115
|
<ReactMarkdown
|
|
83
116
|
remarkPlugins={[remarkGfm]}
|
|
84
117
|
rehypePlugins={[[rehypeHighlight, { detect: false, ignoreMissing: true }]]}
|
|
85
|
-
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
|
+
}}
|
|
86
127
|
>
|
|
87
128
|
{text}
|
|
88
129
|
</ReactMarkdown>
|
|
@@ -887,8 +928,16 @@ const FinalAnswer = memo(function FinalAnswer({
|
|
|
887
928
|
);
|
|
888
929
|
});
|
|
889
930
|
|
|
931
|
+
/** 对话里提问下方的「本轮改了 N 个文件」:来自项目生长这一轮的 commit */
|
|
932
|
+
export interface RoundBadge {
|
|
933
|
+
commit: string;
|
|
934
|
+
files: number;
|
|
935
|
+
add: number;
|
|
936
|
+
del: number;
|
|
937
|
+
}
|
|
938
|
+
|
|
890
939
|
/** 行级 memo:流式期间只有最后一条消息变化,历史行全部跳过重渲染 */
|
|
891
|
-
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 }) {
|
|
892
941
|
const { t } = useI18n();
|
|
893
942
|
const text = message.content
|
|
894
943
|
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
@@ -900,7 +949,7 @@ const UserMessage = memo(function UserMessage({ message, onEditMessage }: { mess
|
|
|
900
949
|
const canEdit = Boolean(onEditMessage && message.id && text);
|
|
901
950
|
if (editing) {
|
|
902
951
|
return (
|
|
903
|
-
<div className="group mt-7 flex w-full flex-col items-end first:mt-0" data-role="user">
|
|
952
|
+
<div className="group mt-7 flex w-full flex-col items-end first:mt-0" data-role="user" data-message-id={message.id}>
|
|
904
953
|
<div className="msg-user-bubble w-full">
|
|
905
954
|
<textarea
|
|
906
955
|
className="w-full resize-none bg-transparent outline-none"
|
|
@@ -940,7 +989,7 @@ const UserMessage = memo(function UserMessage({ message, onEditMessage }: { mess
|
|
|
940
989
|
}
|
|
941
990
|
return (
|
|
942
991
|
// 回合边界:用户消息前留 28px(比回合内 8px 大得多),长对话里一眼找到“这一轮从哪开始”
|
|
943
|
-
<div className="group mt-7 flex w-full flex-col items-end first:mt-0" data-role="user">
|
|
992
|
+
<div className="group mt-7 flex w-full flex-col items-end first:mt-0" data-role="user" data-message-id={message.id}>
|
|
944
993
|
{images.length > 0 && (
|
|
945
994
|
<div className="mb-2 flex max-w-[85%] flex-wrap justify-end gap-2">
|
|
946
995
|
{images.map((image, index) => (
|
|
@@ -956,6 +1005,23 @@ const UserMessage = memo(function UserMessage({ message, onEditMessage }: { mess
|
|
|
956
1005
|
)}
|
|
957
1006
|
{/* 用户消息也走 Markdown:贴进来的代码块/列表不再是一坨纯文本 */}
|
|
958
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
|
+
)}
|
|
959
1025
|
{/* 用户消息:复制 + 原地编辑重发(编辑后模型从这条消息重新回答);时钟在图标左侧(dsh clock=start) */}
|
|
960
1026
|
{text && (
|
|
961
1027
|
<MessageActions
|
|
@@ -1015,6 +1081,8 @@ function TurnBlockView({
|
|
|
1015
1081
|
workingMessage,
|
|
1016
1082
|
onFork,
|
|
1017
1083
|
onEditMessage,
|
|
1084
|
+
roundBadge,
|
|
1085
|
+
onShowRound,
|
|
1018
1086
|
onInspectTool,
|
|
1019
1087
|
onOpenFile,
|
|
1020
1088
|
}: {
|
|
@@ -1034,6 +1102,8 @@ function TurnBlockView({
|
|
|
1034
1102
|
onFork?: (entryId: string) => void;
|
|
1035
1103
|
/** 编辑该回合的用户消息并重新发送(模型从这条消息重新回答) */
|
|
1036
1104
|
onEditMessage?: (entryId: string, text: string) => void;
|
|
1105
|
+
roundBadge?: RoundBadge;
|
|
1106
|
+
onShowRound?: (commit: string) => void;
|
|
1037
1107
|
onInspectTool?: (toolCallId: string) => void;
|
|
1038
1108
|
onOpenFile?: (path: string) => void;
|
|
1039
1109
|
}) {
|
|
@@ -1085,7 +1155,7 @@ function TurnBlockView({
|
|
|
1085
1155
|
|
|
1086
1156
|
return (
|
|
1087
1157
|
<>
|
|
1088
|
-
{user && <UserMessage message={user} onEditMessage={onEditMessage} />}
|
|
1158
|
+
{user && <UserMessage message={user} badge={roundBadge} onShowRound={onShowRound} onEditMessage={onEditMessage} />}
|
|
1089
1159
|
{showProcess && (
|
|
1090
1160
|
<div className="pw-turn">
|
|
1091
1161
|
{contextFiles?.map((resource, i) => (
|
|
@@ -1200,6 +1270,11 @@ function turnBlockPropsEqual(prev: TurnBlockProps, next: TurnBlockProps): boolea
|
|
|
1200
1270
|
prev.contextFiles !== next.contextFiles ||
|
|
1201
1271
|
prev.onFork !== next.onFork ||
|
|
1202
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 ||
|
|
1203
1278
|
prev.onInspectTool !== next.onInspectTool ||
|
|
1204
1279
|
prev.onOpenFile !== next.onOpenFile
|
|
1205
1280
|
) return false;
|
|
@@ -1222,6 +1297,28 @@ function turnBlockPropsEqual(prev: TurnBlockProps, next: TurnBlockProps): boolea
|
|
|
1222
1297
|
|
|
1223
1298
|
const TurnBlock = memo(TurnBlockView, turnBlockPropsEqual);
|
|
1224
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
|
+
|
|
1225
1322
|
export function ChatWindow({
|
|
1226
1323
|
messages,
|
|
1227
1324
|
tools,
|
|
@@ -1240,6 +1337,8 @@ export function ChatWindow({
|
|
|
1240
1337
|
stats,
|
|
1241
1338
|
onOpenTrajectory,
|
|
1242
1339
|
onOpenFile,
|
|
1340
|
+
roundBadges,
|
|
1341
|
+
onShowRound,
|
|
1243
1342
|
}: {
|
|
1244
1343
|
messages: WebMessage[];
|
|
1245
1344
|
tools: Record<string, ToolCardState>;
|
|
@@ -1266,10 +1365,31 @@ export function ChatWindow({
|
|
|
1266
1365
|
/** 在本机编辑器打开工具行涉及的文件 */
|
|
1267
1366
|
onEditMessage?: (entryId: string, text: string) => void;
|
|
1268
1367
|
onOpenFile?: (path: string) => void;
|
|
1368
|
+
roundBadges?: ReadonlyMap<string, RoundBadge>;
|
|
1369
|
+
onShowRound?: (commit: string) => void;
|
|
1269
1370
|
}) {
|
|
1270
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);
|
|
1271
1375
|
const stickToBottom = useRef(true);
|
|
1272
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
|
+
}, []);
|
|
1273
1393
|
// 脱离底部后累计的新内容条数(回到底部按钮上的角标)
|
|
1274
1394
|
const [unseen, setUnseen] = useState(0);
|
|
1275
1395
|
/** 是否已离开底部(state 版:ref 变化不会触发渲染,静态会话翻页时按钮出不来) */
|
|
@@ -1315,7 +1435,71 @@ export function ChatWindow({
|
|
|
1315
1435
|
};
|
|
1316
1436
|
|
|
1317
1437
|
return (
|
|
1318
|
-
<div className="relative flex h-full min-h-0 flex-col">
|
|
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
|
+
))}
|
|
1319
1503
|
<div
|
|
1320
1504
|
ref={scrollRef}
|
|
1321
1505
|
className="min-h-0 flex-1 overflow-y-auto"
|
|
@@ -1349,6 +1533,8 @@ export function ChatWindow({
|
|
|
1349
1533
|
workingMessage={workingMessage}
|
|
1350
1534
|
onFork={onFork}
|
|
1351
1535
|
onEditMessage={onEditMessage}
|
|
1536
|
+
roundBadge={turn.userIndex >= 0 ? roundBadges?.get(messages[turn.userIndex].id ?? "") : undefined}
|
|
1537
|
+
onShowRound={onShowRound}
|
|
1352
1538
|
onInspectTool={onOpenTrajectory}
|
|
1353
1539
|
onOpenFile={onOpenFile}
|
|
1354
1540
|
/>
|
|
@@ -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 : "";
|
|
@@ -466,7 +467,7 @@ export function FileViewer({
|
|
|
466
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>
|