@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
package/src/hooks/useGrowth.ts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* useGrowth
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* useGrowth:项目生长视图的状态(每轮一个 git commit)。
|
|
5
|
+
* 时间轴(拉取 + 实时 commit 去重合并)、轮次导航 / 默认跟随、范围(本轮 / 本会话累计)、
|
|
6
|
+
* 所选轮的文件与变更、当前磁盘全量目录懒加载、展开状态与新变更动画。
|
|
7
7
|
*/
|
|
8
8
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
9
9
|
import { allDirPaths, buildTree, dirsToReveal, graftLazy, type TreeNode } from "@/lib/growth-tree";
|
|
10
|
-
import {
|
|
11
|
-
import type { GrowthChange, GrowthStep } from "@/lib/types";
|
|
10
|
+
import type { GrowthChange, GrowthRound } from "@/lib/types";
|
|
12
11
|
|
|
13
|
-
export type GrowthScope = "
|
|
12
|
+
export type GrowthScope = "round" | "session";
|
|
13
|
+
|
|
14
|
+
/** 时间轴上的一项:pi 的一轮(带轮号)或用户在两轮之间自己的修改(n = null) */
|
|
15
|
+
export interface GrowthEntry extends GrowthRound {
|
|
16
|
+
/** pi 的第几轮(从 1 起);「你的修改」为 null */
|
|
17
|
+
n: number | null;
|
|
18
|
+
}
|
|
14
19
|
|
|
15
20
|
export interface TreeFile {
|
|
16
21
|
path: string;
|
|
@@ -21,30 +26,24 @@ export interface GrowthApi {
|
|
|
21
26
|
available: boolean;
|
|
22
27
|
loading: boolean;
|
|
23
28
|
error: string | null;
|
|
24
|
-
|
|
25
|
-
rounds:
|
|
26
|
-
|
|
27
|
-
baseline: GrowthStep | null;
|
|
28
|
-
latest: GrowthStep | null;
|
|
29
|
-
selected: GrowthStep | null;
|
|
30
|
-
selectedRound: GrowthRound | null;
|
|
29
|
+
/** 本会话的时间轴(时间正序,不含工作区基线) */
|
|
30
|
+
rounds: GrowthEntry[];
|
|
31
|
+
selected: GrowthEntry | null;
|
|
31
32
|
following: boolean;
|
|
32
33
|
scope: GrowthScope;
|
|
33
34
|
/** 所选范围的 from/to tree(查看器取 diff 用) */
|
|
34
35
|
range: { from: string; to: string } | null;
|
|
36
|
+
/** 所选范围的变更 */
|
|
35
37
|
changes: GrowthChange[];
|
|
36
|
-
/** 相对会话基线的全部变更(头部总计) */
|
|
37
|
-
sessionChanges: GrowthChange[];
|
|
38
38
|
tree: TreeNode;
|
|
39
|
-
/**
|
|
39
|
+
/** 所选轮的全部文件路径(快速打开用) */
|
|
40
40
|
filePaths: string[];
|
|
41
41
|
expanded: Set<string>;
|
|
42
|
-
/**
|
|
42
|
+
/** 最新一轮刚改动的路径(入场动画) */
|
|
43
43
|
fresh: Set<string>;
|
|
44
|
-
pending: string[];
|
|
45
44
|
follow(): void;
|
|
46
45
|
/** 选中某一轮(时间轴柱);选到最后一轮等于回到跟随 */
|
|
47
|
-
|
|
46
|
+
select(commit: string): void;
|
|
48
47
|
prev(): void;
|
|
49
48
|
next(): void;
|
|
50
49
|
setScope(scope: GrowthScope): void;
|
|
@@ -54,7 +53,8 @@ export interface GrowthApi {
|
|
|
54
53
|
collapseAll(): void;
|
|
55
54
|
expandLazy(path: string): Promise<void>;
|
|
56
55
|
refresh(): Promise<void>;
|
|
57
|
-
|
|
56
|
+
/** 立即记录:把当前改动提交为「你的修改」;没改动返回 null */
|
|
57
|
+
recordNow(): Promise<GrowthRound | null>;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
export function sessionIdOf(sessionPath: string): string {
|
|
@@ -69,19 +69,23 @@ function cwdKey(cwd: string): string {
|
|
|
69
69
|
return /^[A-Za-z]:\//.test(norm) || norm.startsWith("//") ? norm.toLowerCase() : norm;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
/** 拉取结果在前,本次连接收到的实时 commit 按 commit 哈希去重后接在后面 */
|
|
73
|
+
function mergeRounds(fetched: GrowthRound[], live: GrowthRound[]): GrowthRound[] {
|
|
73
74
|
if (!live.length) return fetched;
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
const known = new Set(fetched.map((round) => round.commit));
|
|
76
|
+
const extra = live.filter((round) => !known.has(round.commit));
|
|
77
|
+
return extra.length ? [...fetched, ...extra] : fetched;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 去掉工作区基线,给 pi 的轮次编号(「你的修改」不占号) */
|
|
81
|
+
export function numberRounds(rounds: GrowthRound[]): GrowthEntry[] {
|
|
82
|
+
let n = 0;
|
|
83
|
+
return rounds.filter((round) => round.kind !== "baseline").map((round) => ({ ...round, n: round.kind === "round" ? (n += 1) : null }));
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
const REVEAL_LIMIT = 200;
|
|
81
87
|
const FRESH_MS = 2500;
|
|
82
|
-
/**
|
|
83
|
-
const NO_PENDING: string[] = [];
|
|
84
|
-
/** 清单 / 变更缓存条数上限(按 tree 哈希键;长会话几百步也就几 MB,超过就淘汰最早的) */
|
|
88
|
+
/** 清单 / 变更缓存条数上限(按 tree 哈希键;长会话几百轮也就几 MB,超过就淘汰最早的) */
|
|
85
89
|
const CACHE_LIMIT = 300;
|
|
86
90
|
function remember<K, V>(map: Map<K, V>, key: K, value: V): void {
|
|
87
91
|
if (map.size >= CACHE_LIMIT) map.delete(map.keys().next().value as K);
|
|
@@ -94,7 +98,7 @@ interface DiskEntry {
|
|
|
94
98
|
size?: number;
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
/**
|
|
101
|
+
/** 磁盘列表里永远不画的目录:版本库内部(提交本来就排除它,节点也没有可看的东西) */
|
|
98
102
|
const HIDDEN_DISK_DIRS = new Set([".git"]);
|
|
99
103
|
|
|
100
104
|
function diskNodes(parent: string, entries: DiskEntry[]): TreeNode[] {
|
|
@@ -133,27 +137,24 @@ async function listDiskEntries(cwd: string, dir: string, signal: AbortSignal): P
|
|
|
133
137
|
export function useGrowth({
|
|
134
138
|
cwd,
|
|
135
139
|
sessionPath,
|
|
136
|
-
|
|
137
|
-
pending,
|
|
140
|
+
liveRounds,
|
|
138
141
|
runtimeError,
|
|
139
142
|
active,
|
|
143
|
+
watchRounds = false,
|
|
140
144
|
connected,
|
|
141
|
-
turnStarts = [],
|
|
142
145
|
}: {
|
|
143
146
|
cwd: string;
|
|
144
147
|
sessionPath: string | null;
|
|
145
|
-
/** usePiWeb
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
pending: string[];
|
|
149
|
-
/** 实时快照失败;成功记录后由事件清除。 */
|
|
148
|
+
/** usePiWeb 在本次连接期间收到的实时 commit */
|
|
149
|
+
liveRounds: GrowthRound[];
|
|
150
|
+
/** 实时提交失败;成功提交后由事件清除。 */
|
|
150
151
|
runtimeError?: string | null;
|
|
151
|
-
/**
|
|
152
|
+
/** 项目栏或查看器打开:拉磁盘目录、文件清单与范围变更 */
|
|
152
153
|
active: boolean;
|
|
153
|
-
/**
|
|
154
|
+
/** 有会话就拉时间轴(对话里「本轮改了 N 个文件」要用),不必等面板打开 */
|
|
155
|
+
watchRounds?: boolean;
|
|
156
|
+
/** SSE 重连后补拉断线期间可能漏掉的 commit。 */
|
|
154
157
|
connected: boolean;
|
|
155
|
-
/** 用户消息的时间戳;用于在旧快照缺少回合标记时恢复轮次边界。 */
|
|
156
|
-
turnStarts?: number[];
|
|
157
158
|
}): GrowthApi {
|
|
158
159
|
const key = JSON.stringify([cwdKey(cwd), sessionPath ?? ""]);
|
|
159
160
|
const requestScope = useMemo(() => ({
|
|
@@ -181,13 +182,12 @@ export function useGrowth({
|
|
|
181
182
|
requestScope.loadingDirs.clear();
|
|
182
183
|
};
|
|
183
184
|
}, [requestScope]);
|
|
184
|
-
const [fetched, setFetched] = useState<{ key: string;
|
|
185
|
+
const [fetched, setFetched] = useState<{ key: string; rounds: GrowthRound[]; available: boolean; error: string | null }>({ key: "", rounds: [], available: true, error: null });
|
|
185
186
|
const [loading, setLoading] = useState(false);
|
|
186
|
-
const [
|
|
187
|
-
const [scope, setScopeState] = useState<GrowthScope>("
|
|
187
|
+
const [selectedCommit, setSelectedCommit] = useState<string | null>(null);
|
|
188
|
+
const [scope, setScopeState] = useState<GrowthScope>("round");
|
|
188
189
|
const [files, setFiles] = useState<{ key: string; tree: string; files: TreeFile[] } | null>(null);
|
|
189
190
|
const [rangeChanges, setRangeChanges] = useState<{ key: string; changes: GrowthChange[]; error?: string } | null>(null);
|
|
190
|
-
const [sessionRange, setSessionRange] = useState<{ key: string; changes: GrowthChange[]; error?: string } | null>(null);
|
|
191
191
|
const [expanded, setExpanded] = useState<Set<string>>(() => new Set());
|
|
192
192
|
const [fresh, setFresh] = useState<Set<string>>(() => new Set());
|
|
193
193
|
const [lazyState, setLazyState] = useState(() => ({ scope: requestScope, children: new Map<string, TreeNode[]>() }));
|
|
@@ -204,12 +204,12 @@ export function useGrowth({
|
|
|
204
204
|
const changesCache = useRef(new Map<string, GrowthChange[]>());
|
|
205
205
|
const collapsedByUser = useRef(new Set<string>());
|
|
206
206
|
const freshTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
207
|
-
const
|
|
207
|
+
const lastRevealed = useRef<string>("");
|
|
208
208
|
const previousConnected = useRef(connected);
|
|
209
209
|
|
|
210
210
|
useEffect(() => {
|
|
211
211
|
const saved = localStorage.getItem("piweb.growth.scope");
|
|
212
|
-
if (saved === "
|
|
212
|
+
if (saved === "round" || saved === "session") setScopeState(saved);
|
|
213
213
|
}, []);
|
|
214
214
|
const setScope = useCallback((s: GrowthScope) => {
|
|
215
215
|
setScopeState(s);
|
|
@@ -241,12 +241,12 @@ export function useGrowth({
|
|
|
241
241
|
|
|
242
242
|
// 切会话:回到跟随、清实时相关状态
|
|
243
243
|
useEffect(() => {
|
|
244
|
-
|
|
244
|
+
setSelectedCommit(null);
|
|
245
245
|
setFresh(new Set());
|
|
246
246
|
setLoading(false);
|
|
247
247
|
setLazyState({ scope: requestScope, children: new Map() });
|
|
248
248
|
if (freshTimer.current) clearTimeout(freshTimer.current);
|
|
249
|
-
|
|
249
|
+
lastRevealed.current = "";
|
|
250
250
|
}, [requestScope]);
|
|
251
251
|
|
|
252
252
|
const refresh = useCallback(async () => {
|
|
@@ -255,7 +255,7 @@ export function useGrowth({
|
|
|
255
255
|
requestScope.refresh?.abort();
|
|
256
256
|
requestScope.refresh = controller;
|
|
257
257
|
if (!cwd) {
|
|
258
|
-
setFetched({ key,
|
|
258
|
+
setFetched({ key, rounds: [], available: true, error: null });
|
|
259
259
|
updateLazy(controller, () => new Map());
|
|
260
260
|
setLoading(false);
|
|
261
261
|
requestScope.controllers.delete(controller);
|
|
@@ -265,17 +265,20 @@ export function useGrowth({
|
|
|
265
265
|
try {
|
|
266
266
|
const params = new URLSearchParams({ cwd });
|
|
267
267
|
if (sessionPath) params.set("session", sessionPath);
|
|
268
|
-
// 两个请求解耦:磁盘目录先到先画,
|
|
268
|
+
// 两个请求解耦:磁盘目录先到先画,git 慢(探测超时等)不拖住目录树
|
|
269
269
|
const growthRequest = sessionPath
|
|
270
|
-
? getJson<{ available: boolean;
|
|
270
|
+
? getJson<{ available: boolean; rounds: GrowthRound[] }>(`/api/growth?${params}`, controller.signal).then(
|
|
271
|
+
(value) => ({ status: "fulfilled" as const, value }),
|
|
272
|
+
(reason: unknown) => ({ status: "rejected" as const, reason }),
|
|
273
|
+
)
|
|
274
|
+
: Promise.resolve({ status: "fulfilled" as const, value: { available: true, rounds: [] as GrowthRound[] } });
|
|
275
|
+
// 面板关着时只拉时间轴,不列磁盘
|
|
276
|
+
const diskRequest = active
|
|
277
|
+
? listDiskEntries(cwd, "", controller.signal).then(
|
|
271
278
|
(value) => ({ status: "fulfilled" as const, value }),
|
|
272
279
|
(reason: unknown) => ({ status: "rejected" as const, reason }),
|
|
273
280
|
)
|
|
274
|
-
: Promise.resolve({ status: "
|
|
275
|
-
const diskRequest = listDiskEntries(cwd, "", controller.signal).then(
|
|
276
|
-
(value) => ({ status: "fulfilled" as const, value }),
|
|
277
|
-
(reason: unknown) => ({ status: "rejected" as const, reason }),
|
|
278
|
-
);
|
|
281
|
+
: Promise.resolve({ status: "skipped" as const });
|
|
279
282
|
void diskRequest.then((diskResult) => {
|
|
280
283
|
if (!isCurrent(controller)) return;
|
|
281
284
|
if (diskResult.status === "fulfilled") {
|
|
@@ -285,9 +288,9 @@ export function useGrowth({
|
|
|
285
288
|
const growthResult = await growthRequest;
|
|
286
289
|
if (!isCurrent(controller)) return;
|
|
287
290
|
if (growthResult.status === "fulfilled") {
|
|
288
|
-
setFetched({ key,
|
|
291
|
+
setFetched({ key, rounds: growthResult.value.rounds ?? [], available: growthResult.value.available !== false, error: null });
|
|
289
292
|
} else {
|
|
290
|
-
setFetched({ key,
|
|
293
|
+
setFetched({ key, rounds: [], available: true, error: growthResult.reason instanceof Error ? growthResult.reason.message : "failed to load" });
|
|
291
294
|
}
|
|
292
295
|
const diskResult = await diskRequest;
|
|
293
296
|
if (!isCurrent(controller)) return;
|
|
@@ -298,22 +301,22 @@ export function useGrowth({
|
|
|
298
301
|
if (isCurrent(controller)) setLoading(false);
|
|
299
302
|
requestScope.controllers.delete(controller);
|
|
300
303
|
}
|
|
301
|
-
}, [cwd, sessionPath, key, beginRequest, isCurrent, requestScope, updateLazy]);
|
|
304
|
+
}, [cwd, sessionPath, key, active, beginRequest, isCurrent, requestScope, updateLazy]);
|
|
302
305
|
|
|
306
|
+
const watching = active || watchRounds;
|
|
303
307
|
useEffect(() => {
|
|
304
|
-
if (
|
|
308
|
+
if (watching) void refresh();
|
|
305
309
|
return () => requestScope.refresh?.abort();
|
|
306
|
-
}, [
|
|
310
|
+
}, [watching, refresh, requestScope]);
|
|
307
311
|
useEffect(() => {
|
|
308
|
-
if (
|
|
312
|
+
if (watching && connected && !previousConnected.current) void refresh();
|
|
309
313
|
previousConnected.current = connected;
|
|
310
|
-
}, [
|
|
314
|
+
}, [watching, connected, refresh]);
|
|
311
315
|
|
|
312
|
-
const
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const diskPulse = latest?.seq ?? 0;
|
|
316
|
+
const rounds = useMemo(() => numberRounds(mergeRounds(fetched.key === key ? fetched.rounds : [], liveRounds)), [fetched, key, liveRounds]);
|
|
317
|
+
const latest = rounds[rounds.length - 1] ?? null;
|
|
318
|
+
// 磁盘目录只在提交了新的一轮时重拉,不用每次渲染都重拉一遍全部目录
|
|
319
|
+
const diskPulse = latest?.commit ?? "";
|
|
317
320
|
useEffect(() => {
|
|
318
321
|
if (!active || !cwd || !latest) return;
|
|
319
322
|
const controller = beginRequest();
|
|
@@ -338,16 +341,17 @@ export function useGrowth({
|
|
|
338
341
|
clearTimeout(timer);
|
|
339
342
|
};
|
|
340
343
|
}, [active, cwd, diskPulse, beginRequest, isCurrent, requestScope, updateLazy]);
|
|
341
|
-
|
|
342
|
-
const following =
|
|
343
|
-
const
|
|
344
|
-
() => (following ?
|
|
345
|
-
[following, rounds,
|
|
344
|
+
|
|
345
|
+
const following = selectedCommit === null;
|
|
346
|
+
const selected = useMemo(
|
|
347
|
+
() => (following ? latest : rounds.find((round) => round.commit === selectedCommit) ?? latest) ?? null,
|
|
348
|
+
[following, latest, rounds, selectedCommit],
|
|
346
349
|
);
|
|
347
|
-
const
|
|
350
|
+
const sessionStart = rounds[0]?.parentTree ?? null;
|
|
348
351
|
|
|
349
|
-
//
|
|
352
|
+
// 所选轮的文件清单;缓存身份包含工作区与会话,迟到结果不写缓存。
|
|
350
353
|
useEffect(() => {
|
|
354
|
+
if (!active) return;
|
|
351
355
|
if (!selected || !cwd) {
|
|
352
356
|
setFiles(null);
|
|
353
357
|
return;
|
|
@@ -374,27 +378,27 @@ export function useGrowth({
|
|
|
374
378
|
controller.abort();
|
|
375
379
|
requestScope.controllers.delete(controller);
|
|
376
380
|
};
|
|
377
|
-
}, [selected, cwd, key, beginRequest, isCurrent, requestScope]);
|
|
381
|
+
}, [active, selected, cwd, key, beginRequest, isCurrent, requestScope]);
|
|
378
382
|
|
|
379
|
-
//
|
|
383
|
+
// 所选范围:本轮 = git diff 上一轮 这一轮;本会话累计 = git diff 会话起点 所选这一轮
|
|
380
384
|
const range = useMemo(() => {
|
|
381
|
-
if (!selected
|
|
382
|
-
if (scope === "
|
|
383
|
-
return { from:
|
|
384
|
-
}, [selected,
|
|
385
|
+
if (!selected) return null;
|
|
386
|
+
if (scope === "round") return { from: selected.parentTree, to: selected.tree };
|
|
387
|
+
return sessionStart ? { from: sessionStart, to: selected.tree } : null;
|
|
388
|
+
}, [selected, scope, sessionStart]);
|
|
389
|
+
// 本轮优先用 commit 自带的变更清单;本会话累计 / 被截断时按 tree 对取
|
|
385
390
|
const inlineChanges = useMemo<GrowthChange[] | null>(() => {
|
|
386
|
-
if (!selected || !
|
|
387
|
-
if (
|
|
388
|
-
if (
|
|
391
|
+
if (!selected || !range) return [];
|
|
392
|
+
if (range.from === range.to) return [];
|
|
393
|
+
if (scope === "round" && !selected.truncated) return selected.changes;
|
|
389
394
|
return null;
|
|
390
|
-
}, [selected,
|
|
395
|
+
}, [selected, range, scope]);
|
|
391
396
|
|
|
392
397
|
const loadRange = useCallback(
|
|
393
398
|
async (from: string, to: string, controller: AbortController): Promise<GrowthChange[]> => {
|
|
394
399
|
const k = `${key}|${from}..${to}`;
|
|
395
400
|
const cached = changesCache.current.get(k);
|
|
396
401
|
if (cached) return cached;
|
|
397
|
-
if (from === to) return [];
|
|
398
402
|
const data = await getJson<{ changes: GrowthChange[] }>(`/api/growth?cwd=${encodeURIComponent(cwd)}&from=${from}&to=${to}&changes=1`, controller.signal);
|
|
399
403
|
if (isCurrent(controller)) remember(changesCache.current, k, data.changes);
|
|
400
404
|
return data.changes;
|
|
@@ -403,7 +407,7 @@ export function useGrowth({
|
|
|
403
407
|
);
|
|
404
408
|
|
|
405
409
|
useEffect(() => {
|
|
406
|
-
if (!range || inlineChanges !== null || !cwd) return;
|
|
410
|
+
if (!active || !range || inlineChanges !== null || !cwd) return;
|
|
407
411
|
const k = `${key}|${range.from}..${range.to}`;
|
|
408
412
|
const controller = beginRequest();
|
|
409
413
|
void loadRange(range.from, range.to, controller)
|
|
@@ -418,7 +422,7 @@ export function useGrowth({
|
|
|
418
422
|
controller.abort();
|
|
419
423
|
requestScope.controllers.delete(controller);
|
|
420
424
|
};
|
|
421
|
-
}, [range, inlineChanges, cwd, key, loadRange, beginRequest, isCurrent, requestScope]);
|
|
425
|
+
}, [active, range, inlineChanges, cwd, key, loadRange, beginRequest, isCurrent, requestScope]);
|
|
422
426
|
|
|
423
427
|
const changes = useMemo<GrowthChange[]>(() => {
|
|
424
428
|
if (inlineChanges !== null) return inlineChanges;
|
|
@@ -426,42 +430,24 @@ export function useGrowth({
|
|
|
426
430
|
return rangeChanges?.key === `${key}|${range.from}..${range.to}` ? rangeChanges.changes : [];
|
|
427
431
|
}, [inlineChanges, range, rangeChanges, key]);
|
|
428
432
|
|
|
429
|
-
// 本会话总计(基线 → 所选步)
|
|
430
|
-
useEffect(() => {
|
|
431
|
-
if (!baseline || !selected || !cwd) return;
|
|
432
|
-
const k = `${key}|${baseline.tree}..${selected.tree}`;
|
|
433
|
-
const controller = beginRequest();
|
|
434
|
-
void loadRange(baseline.tree, selected.tree, controller)
|
|
435
|
-
.then((list) => {
|
|
436
|
-
if (isCurrent(controller)) setSessionRange({ key: k, changes: list });
|
|
437
|
-
})
|
|
438
|
-
.catch((error) => {
|
|
439
|
-
if (isCurrent(controller)) setSessionRange({ key: k, changes: [], error: error instanceof Error ? error.message : "failed to load session changes" });
|
|
440
|
-
})
|
|
441
|
-
.finally(() => requestScope.controllers.delete(controller));
|
|
442
|
-
return () => {
|
|
443
|
-
controller.abort();
|
|
444
|
-
requestScope.controllers.delete(controller);
|
|
445
|
-
};
|
|
446
|
-
}, [baseline, selected, cwd, key, loadRange, beginRequest, isCurrent, requestScope]);
|
|
447
|
-
const sessionChanges = useMemo(() => (baseline && selected && sessionRange?.key === `${key}|${baseline.tree}..${selected.tree}` ? sessionRange.changes : []), [baseline, selected, sessionRange, key]);
|
|
448
|
-
|
|
449
|
-
const shownPending = following ? pending : NO_PENDING;
|
|
450
433
|
const tree = useMemo(() => {
|
|
451
|
-
const base = buildTree(files?.key === key && selected && files.tree === selected.tree ? files.files : [], changes
|
|
434
|
+
const base = buildTree(files?.key === key && selected && files.tree === selected.tree ? files.files : [], changes);
|
|
452
435
|
return graftLazy(base, lazyChildren);
|
|
453
|
-
}, [files, selected, changes,
|
|
436
|
+
}, [files, selected, changes, lazyChildren, key]);
|
|
454
437
|
const filePaths = useMemo(() => {
|
|
455
438
|
const paths = new Set(files?.key === key && selected && files.tree === selected.tree ? files.files.map((file) => file.path) : []);
|
|
456
439
|
for (const children of lazyChildren.values()) for (const child of children) if (child.kind === "file") paths.add(child.path);
|
|
457
440
|
return [...paths];
|
|
458
441
|
}, [files, selected, lazyChildren, key]);
|
|
459
442
|
|
|
460
|
-
//
|
|
443
|
+
// 新的一轮到来(跟随中):自动展开改动路径上的目录(用户手动收起过的除外)+ 入场动画
|
|
461
444
|
useEffect(() => {
|
|
462
|
-
if (!latest || !following || latest.
|
|
463
|
-
|
|
464
|
-
|
|
445
|
+
if (!latest || !following || latest.commit === lastRevealed.current) return;
|
|
446
|
+
const first = lastRevealed.current === "";
|
|
447
|
+
lastRevealed.current = latest.commit;
|
|
448
|
+
// 打开面板时已有的历史不播动画,只对之后新提交的一轮
|
|
449
|
+
if (first && !liveRounds.some((round) => round.commit === latest.commit)) return;
|
|
450
|
+
const list = latest.changes;
|
|
465
451
|
if (!list.length) return;
|
|
466
452
|
const dirs = dirsToReveal(list.slice(0, REVEAL_LIMIT)).filter((d) => !collapsedByUser.current.has(d));
|
|
467
453
|
if (dirs.length) {
|
|
@@ -475,25 +461,12 @@ export function useGrowth({
|
|
|
475
461
|
setFresh(new Set(list.map((c) => c.path)));
|
|
476
462
|
if (freshTimer.current) clearTimeout(freshTimer.current);
|
|
477
463
|
freshTimer.current = setTimeout(() => setFresh(new Set()), FRESH_MS);
|
|
478
|
-
}, [latest, following, persistExpanded]);
|
|
464
|
+
}, [latest, following, liveRounds, persistExpanded]);
|
|
479
465
|
useEffect(() => () => {
|
|
480
466
|
if (freshTimer.current) clearTimeout(freshTimer.current);
|
|
481
467
|
}, []);
|
|
482
468
|
|
|
483
|
-
//
|
|
484
|
-
useEffect(() => {
|
|
485
|
-
if (!shownPending.length) return;
|
|
486
|
-
const dirs = dirsToReveal([], shownPending.slice(0, REVEAL_LIMIT)).filter((d) => !collapsedByUser.current.has(d));
|
|
487
|
-
if (!dirs.length) return;
|
|
488
|
-
setExpanded((prev) => {
|
|
489
|
-
if (dirs.every((d) => prev.has(d))) return prev;
|
|
490
|
-
const next = new Set(prev);
|
|
491
|
-
for (const d of dirs) next.add(d);
|
|
492
|
-
return next;
|
|
493
|
-
});
|
|
494
|
-
}, [shownPending]);
|
|
495
|
-
|
|
496
|
-
// 手动选步:展开这一步改动的目录
|
|
469
|
+
// 手动选轮:展开这一轮改动的目录
|
|
497
470
|
useEffect(() => {
|
|
498
471
|
if (following || !selected) return;
|
|
499
472
|
const list = changes.slice(0, REVEAL_LIMIT);
|
|
@@ -508,23 +481,23 @@ export function useGrowth({
|
|
|
508
481
|
}, [following, selected, changes]);
|
|
509
482
|
|
|
510
483
|
const follow = useCallback(() => {
|
|
511
|
-
|
|
484
|
+
setSelectedCommit(null);
|
|
512
485
|
}, []);
|
|
513
|
-
const
|
|
514
|
-
(
|
|
486
|
+
const select = useCallback(
|
|
487
|
+
(commit: string) => setSelectedCommit(rounds.length && rounds[rounds.length - 1].commit === commit ? null : commit),
|
|
515
488
|
[rounds],
|
|
516
489
|
);
|
|
517
490
|
const prev = useCallback(() => {
|
|
518
|
-
if (!
|
|
519
|
-
const idx = rounds.findIndex((round) => round.
|
|
520
|
-
if (idx > 0)
|
|
521
|
-
}, [
|
|
491
|
+
if (!selected) return;
|
|
492
|
+
const idx = rounds.findIndex((round) => round.commit === selected.commit);
|
|
493
|
+
if (idx > 0) setSelectedCommit(rounds[idx - 1].commit);
|
|
494
|
+
}, [selected, rounds]);
|
|
522
495
|
const next = useCallback(() => {
|
|
523
|
-
if (!
|
|
524
|
-
const idx = rounds.findIndex((round) => round.
|
|
496
|
+
if (!selected) return;
|
|
497
|
+
const idx = rounds.findIndex((round) => round.commit === selected.commit);
|
|
525
498
|
if (idx < 0 || idx >= rounds.length - 1) return;
|
|
526
|
-
|
|
527
|
-
}, [
|
|
499
|
+
setSelectedCommit(idx + 1 === rounds.length - 1 ? null : rounds[idx + 1].commit);
|
|
500
|
+
}, [selected, rounds]);
|
|
528
501
|
|
|
529
502
|
const toggleDir = useCallback(
|
|
530
503
|
(path: string) => {
|
|
@@ -591,7 +564,7 @@ export function useGrowth({
|
|
|
591
564
|
[cwd, requestScope, beginRequest, isCurrent, updateLazy],
|
|
592
565
|
);
|
|
593
566
|
|
|
594
|
-
const
|
|
567
|
+
const recordNow = useCallback(async (): Promise<GrowthRound | null> => {
|
|
595
568
|
if (!sessionPath) return null;
|
|
596
569
|
const controller = beginRequest();
|
|
597
570
|
if (!isCurrent(controller)) return null;
|
|
@@ -599,15 +572,15 @@ export function useGrowth({
|
|
|
599
572
|
const r = await fetch("/api/growth", {
|
|
600
573
|
method: "POST",
|
|
601
574
|
headers: { "Content-Type": "application/json" },
|
|
602
|
-
body: JSON.stringify({ action: "
|
|
575
|
+
body: JSON.stringify({ action: "record", session: sessionIdOf(sessionPath) }),
|
|
603
576
|
signal: controller.signal,
|
|
604
577
|
});
|
|
605
578
|
const j = await r.json();
|
|
606
579
|
if (!isCurrent(controller)) return null;
|
|
607
580
|
if (!r.ok || !j.success) throw new Error(j.error || `request failed (${r.status})`);
|
|
608
|
-
const
|
|
609
|
-
if (
|
|
610
|
-
return
|
|
581
|
+
const round = (j.data?.round ?? null) as GrowthRound | null;
|
|
582
|
+
if (round) setFetched((previous) => isCurrent(controller) && previous.key === key ? { ...previous, rounds: mergeRounds(previous.rounds, [round]) } : previous);
|
|
583
|
+
return round;
|
|
611
584
|
} finally {
|
|
612
585
|
requestScope.controllers.delete(controller);
|
|
613
586
|
}
|
|
@@ -616,25 +589,19 @@ export function useGrowth({
|
|
|
616
589
|
return {
|
|
617
590
|
available: fetched.key === key ? fetched.available : true,
|
|
618
591
|
loading,
|
|
619
|
-
error: runtimeError ?? (fetched.key === key ? fetched.error : null) ?? (range && rangeChanges?.key === `${key}|${range.from}..${range.to}` ? rangeChanges.error : null) ??
|
|
620
|
-
steps,
|
|
592
|
+
error: runtimeError ?? (fetched.key === key ? fetched.error : null) ?? (range && rangeChanges?.key === `${key}|${range.from}..${range.to}` ? rangeChanges.error : null) ?? null,
|
|
621
593
|
rounds,
|
|
622
|
-
baseline,
|
|
623
|
-
latest,
|
|
624
594
|
selected,
|
|
625
|
-
selectedRound,
|
|
626
595
|
following,
|
|
627
596
|
scope,
|
|
628
597
|
range,
|
|
629
598
|
changes,
|
|
630
|
-
sessionChanges,
|
|
631
599
|
tree,
|
|
632
600
|
filePaths,
|
|
633
601
|
expanded,
|
|
634
602
|
fresh,
|
|
635
|
-
pending: shownPending,
|
|
636
603
|
follow,
|
|
637
|
-
|
|
604
|
+
select,
|
|
638
605
|
prev,
|
|
639
606
|
next,
|
|
640
607
|
setScope,
|
|
@@ -644,6 +611,6 @@ export function useGrowth({
|
|
|
644
611
|
collapseAll,
|
|
645
612
|
expandLazy,
|
|
646
613
|
refresh,
|
|
647
|
-
|
|
614
|
+
recordNow,
|
|
648
615
|
};
|
|
649
616
|
}
|
package/src/hooks/usePiWeb.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
7
7
|
import { createEventBatcher, type EventBatcher } from "@/lib/event-batcher";
|
|
8
8
|
import type {
|
|
9
|
-
|
|
9
|
+
GrowthRound,
|
|
10
10
|
SessionSummary,
|
|
11
11
|
TrajEntry,
|
|
12
12
|
WebEvent,
|
|
@@ -60,8 +60,8 @@ export interface PiWebState {
|
|
|
60
60
|
extensionStatuses: Record<string, string>;
|
|
61
61
|
/** 扩展覆盖的「工作中」文案(setWorkingMessage) */
|
|
62
62
|
workingMessage: string | null;
|
|
63
|
-
/**
|
|
64
|
-
growth: {
|
|
63
|
+
/** 项目生长:本次连接期间收到的 commit(完整时间轴由 useGrowth 拉取后按 commit 去重合并) */
|
|
64
|
+
growth: { rounds: GrowthRound[]; error: string | null };
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
export interface SessionListItem extends SessionSummary {
|
|
@@ -233,15 +233,12 @@ export const foldPiWebEvent = (state: PiWebState, evt: WebEvent): PiWebState =>
|
|
|
233
233
|
}
|
|
234
234
|
return s;
|
|
235
235
|
case "growth": {
|
|
236
|
-
const
|
|
237
|
-
s.growth = {
|
|
236
|
+
const rounds = s.growth.rounds.some((x) => x.commit === evt.round.commit) ? s.growth.rounds : [...s.growth.rounds, evt.round];
|
|
237
|
+
s.growth = { rounds, error: null };
|
|
238
238
|
return s;
|
|
239
239
|
}
|
|
240
|
-
case "growth_pending":
|
|
241
|
-
s.growth = { ...s.growth, pending: evt.paths };
|
|
242
|
-
return s;
|
|
243
240
|
case "growth_error":
|
|
244
|
-
s.growth = { ...s.growth,
|
|
241
|
+
s.growth = { ...s.growth, error: evt.message };
|
|
245
242
|
return s;
|
|
246
243
|
case "retry":
|
|
247
244
|
// 只保留最后一次尝试:重试过程在界面上是「重试 3/5」这一条,而不是五行报错
|
|
@@ -318,7 +315,7 @@ const emptyState = (toolPreset: ToolPreset = "standard"): PiWebState => ({
|
|
|
318
315
|
extensionNotices: [],
|
|
319
316
|
extensionStatuses: {},
|
|
320
317
|
workingMessage: null,
|
|
321
|
-
growth: {
|
|
318
|
+
growth: { rounds: [], error: null },
|
|
322
319
|
});
|
|
323
320
|
|
|
324
321
|
function pathKey(value: string): string {
|