@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
|
@@ -1,40 +1,54 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* 头部:工作区名 +
|
|
6
|
-
*
|
|
4
|
+
* 项目栏(侧栏与对话之间的第四列):项目生长 = 每轮一个 git commit。
|
|
5
|
+
* 头部:工作区名 + 立即记录 / 收起;所选轮:提问首行、时间、文件数与行数、在对话里看;
|
|
6
|
+
* 工具行:范围(本轮 / 本会话累计)、只看改动、筛选;中间:项目树(GrowthTree);
|
|
7
|
+
* 底部:时间轴每轮一根柱(高度 = 改动行数,灰色细柱 = 你在两轮之间自己的修改),默认跟随最新一轮。
|
|
7
8
|
*/
|
|
8
9
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
9
10
|
import { GrowthTree } from "@/components/GrowthTree";
|
|
10
11
|
import { IconChevronLeft14, IconChevronRight14, IconCloseOutline14, IconGitOutline16, IconRefreshOutline14, IconSearchOutline16 } from "@/components/icons";
|
|
11
|
-
import type { GrowthApi } from "@/hooks/useGrowth";
|
|
12
|
+
import type { GrowthApi, GrowthEntry } from "@/hooks/useGrowth";
|
|
12
13
|
import { useI18n } from "@/i18n";
|
|
13
14
|
import type { GitInfo } from "@/lib/git-service";
|
|
14
15
|
import type { TreeNode } from "@/lib/growth-tree";
|
|
15
|
-
import type {
|
|
16
|
+
import type { GrowthChange } from "@/lib/types";
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
export function
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (step.kind === "manual") return step.label || t.growthKindManual;
|
|
23
|
-
return step.label || step.toolName || "";
|
|
18
|
+
/** 「第 7 轮 · 把保存按钮改成红色」/「你的修改」 */
|
|
19
|
+
export function roundTitle(round: GrowthEntry, t: Record<string, string>): string {
|
|
20
|
+
if (round.kind !== "round") return t.growthKindUser;
|
|
21
|
+
const label = t.growthRoundLabel.replace("{n}", String(round.n ?? ""));
|
|
22
|
+
return round.title ? `${label} · ${round.title}` : label;
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
const st =
|
|
25
|
+
/** 柱的颜色:你的修改为灰;pi 的一轮按主要改动类型(删除为主红、新增为主绿、否则黄) */
|
|
26
|
+
function roundColor(round: GrowthEntry): string {
|
|
27
|
+
if (round.kind !== "round") return "var(--dsw-label-caption)";
|
|
28
|
+
const st = round.stats;
|
|
29
|
+
if (!st.added && !st.modified && !st.deleted && !st.renamed) return "var(--dsw-border-l3)";
|
|
30
30
|
if (st.deleted && st.deleted >= st.added && st.deleted >= st.modified) return "var(--dsw-danger)";
|
|
31
31
|
if (st.added >= st.modified) return "var(--dsw-success)";
|
|
32
32
|
return "var(--dsw-warn)";
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function changedLines(round: GrowthEntry): number {
|
|
36
|
+
return round.stats.add + round.stats.del;
|
|
37
|
+
}
|
|
38
|
+
|
|
35
39
|
function fmtTime(ts: number): string {
|
|
36
40
|
const d = new Date(ts);
|
|
37
|
-
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}
|
|
41
|
+
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sumChanges(changes: GrowthChange[]): { files: number; add: number; del: number } {
|
|
45
|
+
let add = 0;
|
|
46
|
+
let del = 0;
|
|
47
|
+
for (const c of changes) {
|
|
48
|
+
add += c.add ?? 0;
|
|
49
|
+
del += c.del ?? 0;
|
|
50
|
+
}
|
|
51
|
+
return { files: changes.length, add, del };
|
|
38
52
|
}
|
|
39
53
|
|
|
40
54
|
export function ProjectPanel({
|
|
@@ -42,9 +56,11 @@ export function ProjectPanel({
|
|
|
42
56
|
workspaceName,
|
|
43
57
|
cwd,
|
|
44
58
|
hasSession,
|
|
59
|
+
running = false,
|
|
45
60
|
onOpenFile,
|
|
46
61
|
onReference,
|
|
47
62
|
onOpenEditor,
|
|
63
|
+
onJumpToChat,
|
|
48
64
|
gitRefreshKey = 0,
|
|
49
65
|
onAskCommit,
|
|
50
66
|
onOpenGit,
|
|
@@ -55,9 +71,13 @@ export function ProjectPanel({
|
|
|
55
71
|
workspaceName: string;
|
|
56
72
|
cwd: string;
|
|
57
73
|
hasSession: boolean;
|
|
74
|
+
/** 本会话正在运行:时间轴末尾显示「本轮进行中」 */
|
|
75
|
+
running?: boolean;
|
|
58
76
|
onOpenFile: (node: TreeNode) => void;
|
|
59
77
|
onReference?: (path: string) => void;
|
|
60
78
|
onOpenEditor?: (path: string) => void;
|
|
79
|
+
/** 跳到对话里这一轮的提问(用户消息 entryId) */
|
|
80
|
+
onJumpToChat?: (messageId: string) => void;
|
|
61
81
|
gitRefreshKey?: number;
|
|
62
82
|
onAskCommit?: () => void;
|
|
63
83
|
onOpenGit?: () => void;
|
|
@@ -67,14 +87,15 @@ export function ProjectPanel({
|
|
|
67
87
|
const { t } = useI18n();
|
|
68
88
|
const tt = t as unknown as Record<string, string>;
|
|
69
89
|
const [filter, setFilter] = useState("");
|
|
70
|
-
const [onlyChanges, setOnlyChanges] = useState(
|
|
71
|
-
const [
|
|
90
|
+
const [onlyChanges, setOnlyChanges] = useState(true);
|
|
91
|
+
const [recording, setRecording] = useState(false);
|
|
72
92
|
const [notice, setNotice] = useState("");
|
|
73
93
|
const [gitState, setGitState] = useState<{ cwd: string; info: GitInfo | null; error?: string } | null>(null);
|
|
74
94
|
const git = gitState?.cwd === cwd ? gitState.info : null;
|
|
75
95
|
const gitError = gitState?.cwd === cwd ? gitState.error ?? git?.statusError : undefined;
|
|
76
|
-
useEffect(() => setOnlyChanges(
|
|
77
|
-
const
|
|
96
|
+
useEffect(() => setOnlyChanges(true), [cwd]);
|
|
97
|
+
const { rounds, selected } = growth;
|
|
98
|
+
const latestCommit = rounds[rounds.length - 1]?.commit ?? "";
|
|
78
99
|
useEffect(() => {
|
|
79
100
|
if (!cwd) return;
|
|
80
101
|
const controller = new AbortController();
|
|
@@ -93,28 +114,17 @@ export function ProjectPanel({
|
|
|
93
114
|
clearTimeout(timer);
|
|
94
115
|
controller.abort();
|
|
95
116
|
};
|
|
96
|
-
}, [cwd, gitRefreshKey,
|
|
97
|
-
|
|
98
|
-
const totals = useMemo(() => {
|
|
99
|
-
const out = { added: 0, modified: 0, deleted: 0, renamed: 0 };
|
|
100
|
-
for (const c of growth.scope === "step" ? growth.changes : growth.sessionChanges) {
|
|
101
|
-
if (c.status === "A") out.added += 1;
|
|
102
|
-
else if (c.status === "M") out.modified += 1;
|
|
103
|
-
else if (c.status === "D") out.deleted += 1;
|
|
104
|
-
else out.renamed += 1;
|
|
105
|
-
}
|
|
106
|
-
return out;
|
|
107
|
-
}, [growth.scope, growth.changes, growth.sessionChanges]);
|
|
117
|
+
}, [cwd, gitRefreshKey, latestCommit]);
|
|
108
118
|
|
|
109
|
-
const
|
|
119
|
+
const totals = useMemo(() => sumChanges(growth.changes), [growth.changes]);
|
|
120
|
+
const piRounds = useMemo(() => rounds.filter((round) => round.kind === "round").length, [rounds]);
|
|
121
|
+
const maxLines = useMemo(() => Math.max(1, ...rounds.map(changedLines)), [rounds]);
|
|
110
122
|
const railRef = useRef<HTMLDivElement>(null);
|
|
111
|
-
const maxSteps = useMemo(() => Math.max(1, ...rounds.map((r) => r.steps.filter((step) => step.kind !== "turn").length)), [rounds]);
|
|
112
123
|
useEffect(() => {
|
|
113
|
-
if (!
|
|
114
|
-
railRef.current?.querySelector<HTMLElement>(`[data-
|
|
115
|
-
}, [
|
|
116
|
-
const selectedIdx =
|
|
117
|
-
const roundLabel = selectedRound ? !selectedRound.recorded ? t.growthUnrecorded : selectedRound.steps.every((step) => step.kind === "turn") ? t.growthNoChangesRound : stepTitle(selectedRound.steps.find((step) => step.kind !== "turn") ?? selectedRound.last!, tt) : "";
|
|
124
|
+
if (!selected) return;
|
|
125
|
+
railRef.current?.querySelector<HTMLElement>(`[data-commit="${selected.commit}"]`)?.scrollIntoView({ block: "nearest", inline: "nearest" });
|
|
126
|
+
}, [selected]);
|
|
127
|
+
const selectedIdx = selected ? rounds.findIndex((round) => round.commit === selected.commit) : -1;
|
|
118
128
|
const conflicts = git?.files.filter((file) => file.kind === "conflict").length ?? 0;
|
|
119
129
|
const staged = git?.files.filter((file) => file.kind !== "conflict" && file.indexStatus !== " ").length ?? 0;
|
|
120
130
|
const unstaged = git?.files.filter((file) => file.kind !== "conflict" && file.workStatus !== " " && file.kind !== "untracked").length ?? 0;
|
|
@@ -126,24 +136,21 @@ export function ProjectPanel({
|
|
|
126
136
|
return () => clearTimeout(timer);
|
|
127
137
|
}, [notice]);
|
|
128
138
|
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const snapshotNow = async () => {
|
|
134
|
-
if (snapshotting) return;
|
|
135
|
-
setSnapshotting(true);
|
|
139
|
+
const recordNow = async () => {
|
|
140
|
+
if (recording) return;
|
|
141
|
+
setRecording(true);
|
|
136
142
|
try {
|
|
137
|
-
const
|
|
138
|
-
if (!
|
|
143
|
+
const round = await growth.recordNow();
|
|
144
|
+
if (!round) setNotice(t.growthRecordNone);
|
|
139
145
|
} catch (e) {
|
|
140
146
|
onError(e instanceof Error ? e.message : "failed");
|
|
141
147
|
} finally {
|
|
142
|
-
|
|
148
|
+
setRecording(false);
|
|
143
149
|
}
|
|
144
150
|
};
|
|
145
151
|
|
|
146
|
-
const emptyText = !cwd || !hasSession ? t.growthNoSession : growth.loading && !
|
|
152
|
+
const emptyText = !cwd || !hasSession ? t.growthNoSession : growth.loading && !rounds.length ? t.growthLoading : !growth.available ? t.growthUnavailable : rounds.length ? undefined : t.growthNoRounds;
|
|
153
|
+
const statusText = selected?.status === "aborted" ? t.growthStatusAborted : selected?.status === "error" ? t.growthStatusError : "";
|
|
147
154
|
|
|
148
155
|
return (
|
|
149
156
|
<div className="pw-project flex h-full min-h-0 flex-col" data-testid="project-panel">
|
|
@@ -153,15 +160,15 @@ export function ProjectPanel({
|
|
|
153
160
|
{workspaceName || t.projectPanel}
|
|
154
161
|
</span>
|
|
155
162
|
{hasSession && (
|
|
156
|
-
<button className="icon-btn" style={{ width: 22, height: 22 }} title={t.
|
|
157
|
-
<IconRefreshOutline14 size={13} className={
|
|
163
|
+
<button className="icon-btn" style={{ width: 22, height: 22 }} title={t.growthRecordNow} onClick={() => void recordNow()} disabled={recording}>
|
|
164
|
+
<IconRefreshOutline14 size={13} className={recording ? "piweb-spin" : undefined} />
|
|
158
165
|
</button>
|
|
159
166
|
)}
|
|
160
167
|
<button className="icon-btn" style={{ width: 22, height: 22 }} title={t.close} onClick={onClose}>
|
|
161
168
|
<IconCloseOutline14 size={13} />
|
|
162
169
|
</button>
|
|
163
170
|
</div>
|
|
164
|
-
{/*
|
|
171
|
+
{/* 生长记录不可用(找不到 git 等):明确提示 + 已退化为当前目录结构 */}
|
|
165
172
|
{cwd && hasSession && !growth.available && !growth.loading && (
|
|
166
173
|
<div className="mx-3 mb-2 rounded-lg px-2 py-1" style={{ fontSize: 11.5, color: "var(--dsw-warn)", background: "var(--dsw-hover)" }} role="status">
|
|
167
174
|
{t.growthUnavailableFallback}
|
|
@@ -190,30 +197,39 @@ export function ProjectPanel({
|
|
|
190
197
|
{onOpenGit && <button type="button" className="pw-chip" onClick={onOpenGit}>{t.gitDetails}</button>}
|
|
191
198
|
</div>
|
|
192
199
|
)}
|
|
193
|
-
{/*
|
|
200
|
+
{/* 所选这一轮:提问首行 / 时间 · 文件数 · 行数 / 在对话里看 */}
|
|
201
|
+
{selected && (
|
|
202
|
+
<div className="px-3 pb-2" data-testid="growth-round-summary">
|
|
203
|
+
<div className="truncate" style={{ fontSize: 12.5, fontWeight: 600, color: "var(--dsw-label-primary)" }} title={roundTitle(selected, tt)}>
|
|
204
|
+
{roundTitle(selected, tt)}
|
|
205
|
+
</div>
|
|
206
|
+
<div className="mt-0.5 flex items-center gap-2" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
|
|
207
|
+
<span>{fmtTime(selected.ts)}</span>
|
|
208
|
+
<span>·</span>
|
|
209
|
+
<span>{t.growthRoundFiles.replace("{n}", String(totals.files))}</span>
|
|
210
|
+
<span style={{ color: "var(--dsw-success)" }}>+{totals.add}</span>
|
|
211
|
+
<span style={{ color: "var(--dsw-danger)" }}>−{totals.del}</span>
|
|
212
|
+
{statusText && <span style={{ color: "var(--dsw-warn)" }}>{statusText}</span>}
|
|
213
|
+
<span className="flex-1" />
|
|
214
|
+
{onJumpToChat && selected.promptIds[0] && (
|
|
215
|
+
<button type="button" className="pw-chip" onClick={() => onJumpToChat(selected.promptIds[0])}>
|
|
216
|
+
{t.growthJumpToChat}
|
|
217
|
+
</button>
|
|
218
|
+
)}
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
{/* 范围 + 只看改动 */}
|
|
194
223
|
<div className="flex items-center gap-2 px-3 pb-2" style={{ fontSize: 11.5 }}>
|
|
195
224
|
<span className="pw-seg" role="radiogroup">
|
|
196
|
-
{(["
|
|
225
|
+
{(["round", "session"] as const).map((s) => (
|
|
197
226
|
<button key={s} type="button" role="radio" aria-checked={growth.scope === s} data-active={growth.scope === s} onClick={() => growth.setScope(s)}>
|
|
198
|
-
{s === "
|
|
227
|
+
{s === "round" ? t.growthScopeRound : t.growthScopeSession}
|
|
199
228
|
</button>
|
|
200
229
|
))}
|
|
201
230
|
</span>
|
|
202
|
-
<span className="min-w-0 flex-1 truncate" style={{ color: "var(--dsw-label-
|
|
203
|
-
|
|
204
|
-
<span style={{ color: "var(--dsw-label-secondary)" }}>{notice}</span>
|
|
205
|
-
) : (
|
|
206
|
-
<>
|
|
207
|
-
<span style={{ color: "var(--dsw-success)" }}>+{totals.added}</span>
|
|
208
|
-
{" "}
|
|
209
|
-
<span style={{ color: "var(--dsw-warn)" }}>~{totals.modified}</span>
|
|
210
|
-
{" "}
|
|
211
|
-
<span style={{ color: "var(--dsw-danger)" }}>−{totals.deleted}</span>
|
|
212
|
-
{totals.renamed ? <span style={{ color: "var(--dsw-accent)" }}> ⇄{totals.renamed}</span> : null}
|
|
213
|
-
</>
|
|
214
|
-
)}
|
|
215
|
-
</span>
|
|
216
|
-
<button type="button" className="pw-chip" data-active={onlyChanges} aria-pressed={onlyChanges} onClick={toggleOnly}>
|
|
231
|
+
<span className="min-w-0 flex-1 truncate" style={{ color: "var(--dsw-label-secondary)" }}>{notice}</span>
|
|
232
|
+
<button type="button" className="pw-chip" data-active={onlyChanges} aria-pressed={onlyChanges} onClick={() => setOnlyChanges((value) => !value)}>
|
|
217
233
|
{t.growthOnlyChanges}
|
|
218
234
|
</button>
|
|
219
235
|
</div>
|
|
@@ -241,14 +257,14 @@ export function ProjectPanel({
|
|
|
241
257
|
{growth.error}
|
|
242
258
|
</div>
|
|
243
259
|
)}
|
|
244
|
-
{
|
|
260
|
+
{selected?.truncated && growth.scope === "round" && (
|
|
245
261
|
<div className="mx-3 mb-1 rounded-lg px-2 py-1" style={{ fontSize: 11, color: "var(--dsw-warn)", background: "var(--dsw-hover)" }}>
|
|
246
262
|
{t.growthTruncated}
|
|
247
263
|
</div>
|
|
248
264
|
)}
|
|
249
|
-
{
|
|
265
|
+
{selected && growth.scope === "round" && !growth.changes.length && !emptyText && (
|
|
250
266
|
<div className="mx-3 mb-1 rounded-lg px-2 py-1" style={{ fontSize: 11, color: "var(--dsw-label-caption)", background: "var(--dsw-hover)" }} role="status">
|
|
251
|
-
{t.
|
|
267
|
+
{t.growthNoChangesRound}
|
|
252
268
|
</div>
|
|
253
269
|
)}
|
|
254
270
|
{/* 树 */}
|
|
@@ -257,7 +273,7 @@ export function ProjectPanel({
|
|
|
257
273
|
expanded={growth.expanded}
|
|
258
274
|
fresh={growth.fresh}
|
|
259
275
|
filter={filter}
|
|
260
|
-
onlyChanges={onlyChanges &&
|
|
276
|
+
onlyChanges={onlyChanges && growth.changes.length > 0}
|
|
261
277
|
onToggleDir={growth.toggleDir}
|
|
262
278
|
onExpandLazy={growth.expandLazy}
|
|
263
279
|
onOpenFile={onOpenFile}
|
|
@@ -265,29 +281,30 @@ export function ProjectPanel({
|
|
|
265
281
|
onOpenEditor={onOpenEditor}
|
|
266
282
|
emptyText={emptyText}
|
|
267
283
|
/>
|
|
268
|
-
{/*
|
|
284
|
+
{/* 时间轴 */}
|
|
269
285
|
{hasSession && (
|
|
270
286
|
<div className="pw-timeline hairline-t" data-testid="growth-timeline">
|
|
271
|
-
{
|
|
272
|
-
{rounds.length > 0 && (
|
|
287
|
+
{(rounds.length > 0 || running) && (
|
|
273
288
|
<div ref={railRef} className="pw-tl-rail">
|
|
274
289
|
{rounds.map((round) => {
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
const
|
|
290
|
+
// 对数缩放:几十行和几千行的轮都看得出来,又不会一根柱子压扁其余
|
|
291
|
+
const lines = changedLines(round);
|
|
292
|
+
const h = round.changes.length || lines ? Math.max(5, Math.round(4 + (Math.log1p(lines) / Math.log1p(maxLines)) * 20)) : 3;
|
|
278
293
|
return (
|
|
279
294
|
<button
|
|
280
|
-
key={round.
|
|
295
|
+
key={round.commit}
|
|
281
296
|
type="button"
|
|
282
297
|
className="pw-tl-bar"
|
|
283
|
-
data-
|
|
284
|
-
data-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
298
|
+
data-commit={round.commit}
|
|
299
|
+
data-kind={round.kind}
|
|
300
|
+
data-active={selected?.commit === round.commit || undefined}
|
|
301
|
+
style={{ height: h, background: roundColor(round) }}
|
|
302
|
+
title={`${roundTitle(round, tt)} · ${fmtTime(round.ts)} · ${t.growthRoundFiles.replace("{n}", String(round.changes.length))} +${round.stats.add} −${round.stats.del}`}
|
|
303
|
+
onClick={() => growth.select(round.commit)}
|
|
288
304
|
/>
|
|
289
305
|
);
|
|
290
306
|
})}
|
|
307
|
+
{running && <span className="pw-tl-bar" data-running="" title={t.growthRunning} aria-label={t.growthRunning} />}
|
|
291
308
|
</div>
|
|
292
309
|
)}
|
|
293
310
|
<div className="flex items-center gap-1 px-2 pb-2 pt-1" style={{ fontSize: 11.5 }}>
|
|
@@ -297,12 +314,12 @@ export function ProjectPanel({
|
|
|
297
314
|
<button className="icon-btn" style={{ width: 20, height: 20 }} title={t.growthNext} onClick={growth.next} disabled={selectedIdx < 0 || selectedIdx >= rounds.length - 1}>
|
|
298
315
|
<IconChevronRight14 size={12} />
|
|
299
316
|
</button>
|
|
300
|
-
<span className="min-w-0 flex-1 truncate" style={{ color: "var(--dsw-label-secondary)" }}
|
|
301
|
-
{
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
317
|
+
<span className="min-w-0 flex-1 truncate" style={{ color: "var(--dsw-label-secondary)" }}>
|
|
318
|
+
{selected?.n
|
|
319
|
+
? t.growthRoundPosition.replace("{current}", String(selected.n)).replace("{total}", String(piRounds))
|
|
320
|
+
: selected
|
|
321
|
+
? t.growthKindUser
|
|
322
|
+
: t.growthRoundPosition.replace("{current}", "0").replace("{total}", String(piRounds))}
|
|
306
323
|
</span>
|
|
307
324
|
<button type="button" className="pw-chip" data-active={growth.following} onClick={growth.follow} title={t.growthFollow}>
|
|
308
325
|
{growth.following ? t.growthFollowing : t.growthFollow}
|
|
@@ -36,8 +36,6 @@ interface ImportOutcome {
|
|
|
36
36
|
reason?: string;
|
|
37
37
|
messageCount?: number;
|
|
38
38
|
workspace?: string;
|
|
39
|
-
/** 已成功导入,但源里的运行时注入/加密内容等无法作为对话还原 */
|
|
40
|
-
skipped?: string[];
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
interface ImportReport {
|
|
@@ -62,8 +60,6 @@ function dayOf(ms?: number): string {
|
|
|
62
60
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
63
61
|
}
|
|
64
62
|
|
|
65
|
-
const summaryKey = (summary: Pick<Summary, "source" | "externalId">) => `${summary.source}:${summary.externalId}`;
|
|
66
|
-
|
|
67
63
|
export function SessionImportSection({ cwd }: { cwd: string }) {
|
|
68
64
|
const { t } = useI18n();
|
|
69
65
|
const [summaries, setSummaries] = useState<Summary[]>([]);
|
|
@@ -87,14 +83,9 @@ export function SessionImportSection({ cwd }: { cwd: string }) {
|
|
|
87
83
|
setFailure(json.error || t.toastError);
|
|
88
84
|
return;
|
|
89
85
|
}
|
|
90
|
-
|
|
91
|
-
const nextImported = new Set<string>(json.data.imported ?? []);
|
|
92
|
-
const selectable = new Set(nextSummaries.map(summaryKey));
|
|
93
|
-
setSummaries(nextSummaries);
|
|
86
|
+
setSummaries(json.data.sessions ?? []);
|
|
94
87
|
setErrors(json.data.errors ?? []);
|
|
95
|
-
setImportedKeys(
|
|
96
|
-
// 重新扫描后,源会话可能已经消失或被另一个请求导入;不要留下幽灵选择
|
|
97
|
-
setSelected((previous) => new Set([...previous].filter((item) => selectable.has(item) && !nextImported.has(item))));
|
|
88
|
+
setImportedKeys(new Set<string>(json.data.imported ?? []));
|
|
98
89
|
} catch (error) {
|
|
99
90
|
setFailure(error instanceof Error ? error.message : String(error));
|
|
100
91
|
} finally {
|
|
@@ -118,6 +109,8 @@ export function SessionImportSection({ cwd }: { cwd: string }) {
|
|
|
118
109
|
void scan();
|
|
119
110
|
}, [scan]);
|
|
120
111
|
|
|
112
|
+
const key = (summary: Summary) => `${summary.source}:${summary.externalId}`;
|
|
113
|
+
|
|
121
114
|
const groups = useMemo(() => {
|
|
122
115
|
const bySource = new Map<string, Summary[]>();
|
|
123
116
|
for (const summary of summaries) {
|
|
@@ -127,30 +120,27 @@ export function SessionImportSection({ cwd }: { cwd: string }) {
|
|
|
127
120
|
}
|
|
128
121
|
return SOURCE_ORDER.filter((source) => bySource.has(source)).map((source) => {
|
|
129
122
|
const rows = [...(bySource.get(source) as Summary[])].sort((left, right) => (right.updatedAt ?? right.createdAt ?? 0) - (left.updatedAt ?? left.createdAt ?? 0));
|
|
130
|
-
return { source, rows, imported: rows.filter((row) => importedKeys.has(
|
|
123
|
+
return { source, rows, imported: rows.filter((row) => importedKeys.has(key(row))).length };
|
|
131
124
|
});
|
|
132
125
|
}, [summaries, importedKeys]);
|
|
133
126
|
|
|
134
127
|
/**
|
|
135
128
|
* 一次只显示一个来源:六个工具各列 15 条就是 90 行,全铺出来既难找也没必要。
|
|
136
|
-
*
|
|
137
|
-
* 直接派生有效来源,不用 effect 回填,避免初始化 effect 覆盖用户刚完成的点击。
|
|
129
|
+
* 默认落在第一个有会话的来源上(SOURCE_ORDER 的顺序),用户切换后不被扫描结果重置。
|
|
138
130
|
*/
|
|
139
131
|
const [activeSource, setActiveSource] = useState("");
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const visibleSource = activeSource && groups.some((group) => group.source === activeSource) ? activeSource : defaultSource;
|
|
146
|
-
const activeGroup = groups.find((group) => group.source === visibleSource);
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (activeSource && groups.some((group) => group.source === activeSource)) return;
|
|
134
|
+
setActiveSource(groups[0]?.source ?? "");
|
|
135
|
+
}, [groups, activeSource]);
|
|
136
|
+
const activeGroup = groups.find((group) => group.source === activeSource);
|
|
147
137
|
|
|
148
138
|
const toggle = (summary: Summary) => {
|
|
149
|
-
if (importedKeys.has(
|
|
139
|
+
if (importedKeys.has(key(summary))) return;
|
|
150
140
|
setSelected((previous) => {
|
|
151
141
|
const next = new Set(previous);
|
|
152
|
-
if (next.has(
|
|
153
|
-
else next.add(
|
|
142
|
+
if (next.has(key(summary))) next.delete(key(summary));
|
|
143
|
+
else next.add(key(summary));
|
|
154
144
|
return next;
|
|
155
145
|
});
|
|
156
146
|
};
|
|
@@ -159,9 +149,9 @@ export function SessionImportSection({ cwd }: { cwd: string }) {
|
|
|
159
149
|
setSelected((previous) => {
|
|
160
150
|
const next = new Set(previous);
|
|
161
151
|
for (const row of rows) {
|
|
162
|
-
if (importedKeys.has(
|
|
163
|
-
if (checked) next.add(
|
|
164
|
-
else next.delete(
|
|
152
|
+
if (importedKeys.has(key(row))) continue;
|
|
153
|
+
if (checked) next.add(key(row));
|
|
154
|
+
else next.delete(key(row));
|
|
165
155
|
}
|
|
166
156
|
return next;
|
|
167
157
|
});
|
|
@@ -169,7 +159,7 @@ export function SessionImportSection({ cwd }: { cwd: string }) {
|
|
|
169
159
|
|
|
170
160
|
const run = async () => {
|
|
171
161
|
if (!selected.size) return;
|
|
172
|
-
const items = summaries.filter((summary) => selected.has(
|
|
162
|
+
const items = summaries.filter((summary) => selected.has(key(summary))).map((summary) => ({ source: summary.source, externalId: summary.externalId }));
|
|
173
163
|
setImporting(true);
|
|
174
164
|
setFailure(null);
|
|
175
165
|
setReport(null);
|
|
@@ -208,7 +198,6 @@ export function SessionImportSection({ cwd }: { cwd: string }) {
|
|
|
208
198
|
|
|
209
199
|
const failures = report?.results.filter((result) => result.status === "failed") ?? [];
|
|
210
200
|
const skipped = report?.results.filter((result) => result.status === "skipped") ?? [];
|
|
211
|
-
const importedWithNotes = report?.results.filter((result) => result.status === "imported" && result.skipped?.length) ?? [];
|
|
212
201
|
|
|
213
202
|
return (
|
|
214
203
|
<div className="flex flex-col gap-4" data-testid="session-import">
|
|
@@ -288,11 +277,6 @@ export function SessionImportSection({ cwd }: { cwd: string }) {
|
|
|
288
277
|
{sourceLabel(result.source)} · {result.title || result.externalId} — {result.reason}
|
|
289
278
|
</div>
|
|
290
279
|
))}
|
|
291
|
-
{importedWithNotes.map((result) => (
|
|
292
|
-
<div key={`note-${result.source}-${result.externalId}`} className="mt-1" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
|
|
293
|
-
{sourceLabel(result.source)} · {result.title || result.externalId} — {result.skipped?.join(";")}
|
|
294
|
-
</div>
|
|
295
|
-
))}
|
|
296
280
|
{failures.map((result) => (
|
|
297
281
|
<div key={`fail-${result.source}-${result.externalId}`} className="mt-1" style={{ fontSize: 11.5, color: "var(--dsw-danger)" }}>
|
|
298
282
|
{sourceLabel(result.source)} · {result.title || result.externalId} — {result.reason}
|
|
@@ -307,7 +291,7 @@ export function SessionImportSection({ cwd }: { cwd: string }) {
|
|
|
307
291
|
{groups.length > 0 && (
|
|
308
292
|
<div className="flex flex-wrap items-center gap-1.5" role="tablist" aria-label={t.importPickSource} data-testid="import-source-tabs">
|
|
309
293
|
{groups.map((group) => {
|
|
310
|
-
const active = group.source ===
|
|
294
|
+
const active = group.source === activeSource;
|
|
311
295
|
return (
|
|
312
296
|
<button
|
|
313
297
|
key={group.source}
|
|
@@ -348,7 +332,7 @@ export function SessionImportSection({ cwd }: { cwd: string }) {
|
|
|
348
332
|
</div>
|
|
349
333
|
<div className="flex flex-col gap-1.5">
|
|
350
334
|
{activeGroup.rows.map((row) => {
|
|
351
|
-
const rowKey =
|
|
335
|
+
const rowKey = key(row);
|
|
352
336
|
const done = importedKeys.has(rowKey);
|
|
353
337
|
const checked = selected.has(rowKey);
|
|
354
338
|
return (
|