@xgspz/cli-ui 0.0.0-stage → 0.6.14
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/CHANGELOG.md +156 -0
- package/README.md +512 -2
- package/dist/cli-ui-embed-D-3DvyTN.js +50279 -0
- package/dist/cli-ui-index-C34qFb4u.js +11209 -0
- package/dist/cli-ui-jszip.min-BObcE2nn.js +26 -0
- package/dist/cli-ui-jszip.min-DojbZPiE.js +2324 -0
- package/dist/cli-ui-xlsx-JVLtx0qR.js +93 -0
- package/dist/cli-ui.css +32 -0
- package/dist/cli-ui.js +17 -0
- package/package.json +60 -4
- package/src/board/BrowserPane.tsx +969 -0
- package/src/board/ChangesPane.tsx +185 -0
- package/src/board/CliBoard.tsx +484 -0
- package/src/board/FileViewer.tsx +721 -0
- package/src/board/FilesPane.tsx +262 -0
- package/src/board/HtmlReadingPreview.tsx +52 -0
- package/src/board/MarkdownPreview.tsx +134 -0
- package/src/board/PanelControls.tsx +40 -0
- package/src/board/blocks.tsx +97 -0
- package/src/board/context.ts +65 -0
- package/src/board/diffLines.ts +46 -0
- package/src/board/fileKind.ts +250 -0
- package/src/board/fileOps.ts +85 -0
- package/src/board/fileScope.ts +32 -0
- package/src/board/fileSync.ts +351 -0
- package/src/board/fileSyncCache.ts +250 -0
- package/src/board/fileSyncProcessor.ts +72 -0
- package/src/board/fileSyncQueue.ts +56 -0
- package/src/board/fileSyncTransport.ts +85 -0
- package/src/board/fileWatch.ts +211 -0
- package/src/board/folderDownload.ts +211 -0
- package/src/board/frameQuote.ts +52 -0
- package/src/board/htmlReading.ts +148 -0
- package/src/board/panels.ts +33 -0
- package/src/board/textLoad.ts +76 -0
- package/src/board/useBoardUpload.tsx +51 -0
- package/src/board/useBoardVisibility.ts +35 -0
- package/src/board/workspace.ts +200 -0
- package/src/board/zip.ts +85 -0
- package/src/chat/CliChat.tsx +932 -0
- package/src/chat/Composer.tsx +239 -0
- package/src/chat/GroupChat.tsx +113 -0
- package/src/chat/RunControls.tsx +196 -0
- package/src/chat/configuration.ts +15 -0
- package/src/chat/draft.ts +49 -0
- package/src/chat/export.ts +34 -0
- package/src/chat/queueEdit.ts +27 -0
- package/src/chat/useExecutionConfig.ts +93 -0
- package/src/components.tsx +13 -0
- package/src/core/blocks.ts +205 -0
- package/src/core/callbacks.ts +8 -0
- package/src/core/client.ts +822 -0
- package/src/core/config.ts +252 -0
- package/src/core/conversation.ts +577 -0
- package/src/core/documentUpload.ts +196 -0
- package/src/core/execution.ts +24 -0
- package/src/core/id.ts +15 -0
- package/src/core/identity.ts +21 -0
- package/src/core/provider.tsx +141 -0
- package/src/core/sha256.ts +21 -0
- package/src/core/sse.ts +51 -0
- package/src/core/store.ts +1103 -0
- package/src/core/subagents.ts +78 -0
- package/src/core/textDecode.ts +24 -0
- package/src/core/types.ts +505 -0
- package/src/core/xlsx.ts +142 -0
- package/src/elements.tsx +376 -0
- package/src/embed.ts +56 -0
- package/src/index.ts +56 -0
- package/src/layout/CliSplit.tsx +53 -0
- package/src/layout/split.ts +251 -0
- package/src/msglist/CliMsgList.tsx +422 -0
- package/src/operator/branchFlow.ts +35 -0
- package/src/operator/index.ts +9 -0
- package/src/operator/liveStream.ts +1 -0
- package/src/operator/node/AgentConnect.tsx +619 -0
- package/src/operator/node/AppearancePanel.tsx +35 -0
- package/src/operator/node/BranchHistory.tsx +67 -0
- package/src/operator/node/BranchNavigator.tsx +36 -0
- package/src/operator/node/BrowserPane.tsx +128 -0
- package/src/operator/node/BrowserPopup.tsx +22 -0
- package/src/operator/node/Chat.tsx +538 -0
- package/src/operator/node/CommandPalette.tsx +213 -0
- package/src/operator/node/Composer.tsx +316 -0
- package/src/operator/node/InsightsView.tsx +831 -0
- package/src/operator/node/Layout.tsx +192 -0
- package/src/operator/node/Markdown.tsx +61 -0
- package/src/operator/node/Modal.tsx +179 -0
- package/src/operator/node/NeedsYouMenu.tsx +54 -0
- package/src/operator/node/OnboardingWizard.tsx +248 -0
- package/src/operator/node/Orchestrator.tsx +644 -0
- package/src/operator/node/ProjectLanding.tsx +86 -0
- package/src/operator/node/ProjectsColumn.tsx +137 -0
- package/src/operator/node/ProviderAuth.tsx +559 -0
- package/src/operator/node/RunControls.tsx +2 -0
- package/src/operator/node/RunSwitch.tsx +113 -0
- package/src/operator/node/Services.tsx +245 -0
- package/src/operator/node/SessionRail.tsx +299 -0
- package/src/operator/node/SessionView.tsx +292 -0
- package/src/operator/node/SettingsView.tsx +690 -0
- package/src/operator/node/TaskBoard.tsx +352 -0
- package/src/operator/node/TaskChanges.tsx +526 -0
- package/src/operator/node/TaskHero.tsx +50 -0
- package/src/operator/node/TasksColumn.tsx +200 -0
- package/src/operator/node/Terminal.tsx +143 -0
- package/src/operator/node/Transcript.tsx +485 -0
- package/src/operator/node/ViewFile.tsx +79 -0
- package/src/operator/node/Welcome.tsx +101 -0
- package/src/operator/node/WorktreePrune.tsx +333 -0
- package/src/operator/node/agents.ts +58 -0
- package/src/operator/node/api.ts +57 -0
- package/src/operator/node/boardBackend.ts +63 -0
- package/src/operator/node/boardEvents.ts +40 -0
- package/src/operator/node/boardInteraction.ts +15 -0
- package/src/operator/node/boardState.ts +74 -0
- package/src/operator/node/boardWorkspace.ts +15 -0
- package/src/operator/node/boundedPoll.ts +47 -0
- package/src/operator/node/branchFlow.ts +1 -0
- package/src/operator/node/composerDraft.ts +78 -0
- package/src/operator/node/fileLinks.ts +81 -0
- package/src/operator/node/format.ts +292 -0
- package/src/operator/node/github.tsx +232 -0
- package/src/operator/node/icons.tsx +88 -0
- package/src/operator/node/liveStream.ts +323 -0
- package/src/operator/node/modals.tsx +766 -0
- package/src/operator/node/navHistory.ts +70 -0
- package/src/operator/node/persist.ts +26 -0
- package/src/operator/node/protocol/agents/optIn.ts +9 -0
- package/src/operator/node/protocol/agents/types.ts +394 -0
- package/src/operator/node/protocol/approvalFailure.ts +77 -0
- package/src/operator/node/protocol/authFailure.ts +66 -0
- package/src/operator/node/protocol/customModels.ts +23 -0
- package/src/operator/node/protocol/events.ts +13 -0
- package/src/operator/node/protocol/features.ts +70 -0
- package/src/operator/node/protocol/internalUsage.ts +1 -0
- package/src/operator/node/protocol/promptLimits.ts +52 -0
- package/src/operator/node/protocol/storageRetention.ts +13 -0
- package/src/operator/node/protocol/types.ts +335 -0
- package/src/operator/node/protocol/usageLimit.ts +72 -0
- package/src/operator/node/shared.tsx +89 -0
- package/src/operator/node/transcriptModel.ts +129 -0
- package/src/operator/node/types.ts +305 -0
- package/src/operator/node/useGlobalEvents.ts +97 -0
- package/src/operator/node/useOrchestrator.ts +697 -0
- package/src/operator/node/usePrefs.ts +114 -0
- package/src/operator/node/useRecaps.ts +102 -0
- package/src/operator/node/useTaskStream.ts +286 -0
- package/src/operator.base.css +1949 -0
- package/src/operator.css +1949 -0
- package/src/styles.css +579 -0
- package/src/ui/ErrorBoundary.tsx +38 -0
- package/src/ui/Markdown.tsx +60 -0
- package/src/ui/Popover.tsx +95 -0
- package/src/ui/QueuedMessages.tsx +110 -0
- package/src/ui/ReferenceCard.tsx +28 -0
- package/src/ui/SubagentChat.tsx +233 -0
- package/src/ui/TextBlock.tsx +51 -0
- package/src/ui/Transcript.tsx +435 -0
- package/src/ui/UploadProgress.tsx +46 -0
- package/src/ui/WorkspaceDrawer.tsx +64 -0
- package/src/ui/WorkspaceTabs.tsx +60 -0
- package/src/ui/fileLinks.ts +52 -0
- package/src/ui/format.ts +103 -0
- package/src/ui/icons.tsx +92 -0
- package/src/ui/reference.ts +40 -0
- package/src/ui/shared.tsx +38 -0
- package/src/ui/transcriptModel.ts +151 -0
- package/src/ui/useChatScrollbar.ts +29 -0
- package/src/ui/useComposerAutosize.ts +54 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// 改动:移植自 Operator 的 TaskChanges(只读部分:分支、统计、文件列表、逐文件 diff)。
|
|
2
|
+
// 合并 / 解决冲突 / 建 PR 是 Operator 里的操作,经中枢的对话任务不提供。
|
|
3
|
+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { CenterError, type BoardBackend } from "../core/client";
|
|
5
|
+
import type { DiffFile, DiffResult } from "../core/types";
|
|
6
|
+
import { countChanges, diffLineKinds, hunkLines } from "./diffLines";
|
|
7
|
+
|
|
8
|
+
const STATUS_LABEL: Record<string, string> = { A: "新增", M: "修改", D: "删除", R: "重命名", "?": "未跟踪" };
|
|
9
|
+
const COLLAPSE_LINES = 400;
|
|
10
|
+
/** 一次画多少个文件的 diff:改了几千个文件时不一次塞进页面,往下点「再显示」。 */
|
|
11
|
+
const RENDER_BATCH = 150;
|
|
12
|
+
|
|
13
|
+
/** 只给了一整段 unified diff 的节点:按文件切开。 */
|
|
14
|
+
export function splitUnified(diff: string): DiffFile[] {
|
|
15
|
+
const files: DiffFile[] = [];
|
|
16
|
+
for (const chunk of diff.split(/^(?=diff --git )/m)) {
|
|
17
|
+
if (!chunk.startsWith("diff --git ")) continue;
|
|
18
|
+
const head = /^diff --git a\/(.+?) b\/(.+)$/m.exec(chunk);
|
|
19
|
+
const path = head?.[2] ?? "?";
|
|
20
|
+
const status = /^new file mode/m.test(chunk) ? "A" : /^deleted file mode/m.test(chunk) ? "D" : /^rename from/m.test(chunk) ? "R" : "M";
|
|
21
|
+
const { additions, deletions } = countChanges(chunk);
|
|
22
|
+
files.push({ path, status, additions, deletions, binary: /^Binary files /m.test(chunk), patch: chunk });
|
|
23
|
+
}
|
|
24
|
+
return files;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const FileDiff = memo(function FileDiff({ file: f, userToggled, onToggle, refs, onOpen }: {
|
|
28
|
+
file: DiffFile; userToggled: boolean; onToggle: (path: string) => void;
|
|
29
|
+
refs: { current: Record<string, HTMLDivElement | null> }; onOpen?: (path: string) => void;
|
|
30
|
+
}) {
|
|
31
|
+
const lines = useMemo(() => (f.binary || f.omitted ? [] : hunkLines(f.patch)), [f]);
|
|
32
|
+
const kinds = useMemo(() => diffLineKinds(lines), [lines]);
|
|
33
|
+
const big = lines.length > COLLAPSE_LINES;
|
|
34
|
+
const isCollapsed = userToggled ? !big : big;
|
|
35
|
+
const est = Math.round(34 + (isCollapsed ? 38 : Math.max(1, lines.length) * 18.6));
|
|
36
|
+
return (
|
|
37
|
+
<div className="tc-file" style={{ containIntrinsicSize: `auto ${est}px` }} ref={(el) => { refs.current[f.path] = el; }}>
|
|
38
|
+
<button className="tc-fhead" onClick={() => onToggle(f.path)}>
|
|
39
|
+
<span className={`tc-chev ${isCollapsed ? "" : "open"}`}>▸</span>
|
|
40
|
+
<span className={`tc-st s-${f.status === "?" ? "new" : f.status}`}>{f.status}</span>
|
|
41
|
+
<span className="tc-fpath">{f.path}</span>
|
|
42
|
+
{onOpen && f.status !== "D" && (
|
|
43
|
+
<span className="cli-tc-open" role="button" tabIndex={0} title="打开文件" onClick={(e) => { e.stopPropagation(); onOpen(f.path); }} onKeyDown={(e) => { if (e.key === "Enter") { e.stopPropagation(); onOpen(f.path); } }}>打开</span>
|
|
44
|
+
)}
|
|
45
|
+
<span className="tc-cnt"><b className="add">+{f.additions}</b> <b className="del">−{f.deletions}</b></span>
|
|
46
|
+
</button>
|
|
47
|
+
{isCollapsed ? (
|
|
48
|
+
big && <button className="tc-bigdiff" onClick={() => onToggle(f.path)}>大型 diff({lines.length.toLocaleString()} 行)—— 点击展开</button>
|
|
49
|
+
) : (
|
|
50
|
+
<div className="tc-hunks">
|
|
51
|
+
{f.omitted ? <div className="tc-empty">改动太多,这个文件的 diff 没有带上{onOpen && f.status !== "D" ? ",可以点「打开」看文件" : ""}。</div>
|
|
52
|
+
: f.binary ? <div className="tc-empty">二进制文件 —— 不予显示</div>
|
|
53
|
+
: lines.length === 0 ? <div className="tc-empty">没有文本改动(仅权限或重命名)。</div>
|
|
54
|
+
: lines.map((ln, i) => <div key={i} className={`dl ${kinds[i]}`}>{ln || " "}</div>)}
|
|
55
|
+
{f.truncated && <div className="tc-empty">… 文件 diff 已截断</div>}
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/** 刷新信号挤在一起时(轮末收尾 + 文件变化)合成一次读:diff 在节点上要跑一组 git 命令。 */
|
|
63
|
+
const REFRESH_COALESCE_MS = 250;
|
|
64
|
+
/** 节点暂时连不上、超时、网关错误:隔一会儿自动再读,几次之后只留「重试」。 */
|
|
65
|
+
const RETRY_DELAYS_MS = [3_000, 10_000, 30_000];
|
|
66
|
+
/** git 读失败、工作区已清理之类不会自己好,不自动重试。 */
|
|
67
|
+
function transient(error: unknown): boolean {
|
|
68
|
+
if (!(error instanceof CenterError)) return false;
|
|
69
|
+
return error.code === "NETWORK" || error.code === "node_unreachable" || error.code === "node_timeout" || [429, 502, 503, 504].includes(error.status);
|
|
70
|
+
}
|
|
71
|
+
const clock = (at: number) => new Date(at).toLocaleTimeString("zh-CN", { hour12: false });
|
|
72
|
+
|
|
73
|
+
/** settled:轮次收尾计数(store 的 ConvState.settled),每变一次重读一次。 */
|
|
74
|
+
export function ChangesPane({ backend, jobId, refreshKey, settled = 0, onOpen }: { backend: BoardBackend; jobId: string; refreshKey: number; settled?: number; onOpen?: (path: string) => void }) {
|
|
75
|
+
const [data, setData] = useState<DiffResult | null>(null);
|
|
76
|
+
const [error, setError] = useState<string | null>(null);
|
|
77
|
+
const [loading, setLoading] = useState(false);
|
|
78
|
+
const [manual, setManual] = useState(0);
|
|
79
|
+
const [toggled, setToggled] = useState<Set<string>>(new Set());
|
|
80
|
+
const [active, setActive] = useState<string | null>(null);
|
|
81
|
+
const [shown, setShown] = useState(RENDER_BATCH);
|
|
82
|
+
const secRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
|
83
|
+
const scrollRef = useRef<HTMLDivElement | null>(null);
|
|
84
|
+
|
|
85
|
+
// 上次读成功的时间:刷新失败时仍显示上次的结果,并写明是什么时候的。
|
|
86
|
+
const [loadedAt, setLoadedAt] = useState<number | null>(null);
|
|
87
|
+
// 自动重试:retrying = 已排好下一次;auto 每次到点 +1 触发重读。新的刷新信号(轮末、文件变化、手动)重新计数。
|
|
88
|
+
const [retrying, setRetrying] = useState(false);
|
|
89
|
+
const [auto, setAuto] = useState(0);
|
|
90
|
+
const attempts = useRef(0);
|
|
91
|
+
const trigger = `${refreshKey}|${settled}|${manual}`;
|
|
92
|
+
const lastTrigger = useRef(trigger);
|
|
93
|
+
// 首次读可以和同时打开的其他看板共用请求;之后每次刷新都单独发(fresh),不拿刷新之前就发出的结果。
|
|
94
|
+
const loadedOnce = useRef(false);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
let alive = true;
|
|
97
|
+
let retryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
98
|
+
if (lastTrigger.current !== trigger) { lastTrigger.current = trigger; attempts.current = 0; }
|
|
99
|
+
const refresh = loadedOnce.current;
|
|
100
|
+
loadedOnce.current = true;
|
|
101
|
+
setLoading(true);
|
|
102
|
+
setRetrying(false);
|
|
103
|
+
const load = () => backend.diff(jobId, refresh ? { fresh: true } : undefined).then((d) => {
|
|
104
|
+
// 自定义后端回了空值:当失败处理,不能停在「读取改动…」。
|
|
105
|
+
if (!d || typeof d !== "object") throw new CenterError("改动接口没有返回内容,请稍后重试", "bad_response", 200);
|
|
106
|
+
return d;
|
|
107
|
+
}).then((d) => {
|
|
108
|
+
if (!alive) return;
|
|
109
|
+
attempts.current = 0;
|
|
110
|
+
setData(d); setError(null); setLoadedAt(Date.now()); setLoading(false);
|
|
111
|
+
}, (e: unknown) => {
|
|
112
|
+
if (!alive) return;
|
|
113
|
+
setError(e instanceof Error ? e.message : String(e)); setLoading(false);
|
|
114
|
+
if (transient(e) && attempts.current < RETRY_DELAYS_MS.length) {
|
|
115
|
+
setRetrying(true);
|
|
116
|
+
retryTimer = setTimeout(() => { if (alive) setAuto((n) => n + 1); }, RETRY_DELAYS_MS[attempts.current++]);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
if (!refresh) { void load(); return () => { alive = false; clearTimeout(retryTimer); }; }
|
|
120
|
+
const timer = setTimeout(load, REFRESH_COALESCE_MS);
|
|
121
|
+
return () => { alive = false; clearTimeout(timer); clearTimeout(retryTimer); };
|
|
122
|
+
}, [backend, jobId, refreshKey, settled, manual, auto]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
123
|
+
|
|
124
|
+
const toggle = useCallback((path: string) => setToggled((s) => { const n = new Set(s); if (n.has(path)) n.delete(path); else n.add(path); return n; }), []);
|
|
125
|
+
const jump = (path: string) => {
|
|
126
|
+
setActive(path);
|
|
127
|
+
// 跳到还没画出来的文件:先把它画出来。
|
|
128
|
+
const at = files.findIndex((f) => f.path === path);
|
|
129
|
+
if (at >= shown) setShown(at + RENDER_BATCH);
|
|
130
|
+
const el = secRefs.current[path];
|
|
131
|
+
const box = scrollRef.current;
|
|
132
|
+
if (el && box) box.scrollTo({ top: el.offsetTop - box.offsetTop, behavior: "smooth" });
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const files = useMemo(() => data?.files ?? (data?.diff || data?.patch ? splitUnified(data.diff ?? data.patch ?? "") : []), [data]);
|
|
136
|
+
|
|
137
|
+
const retryNote = retrying ? ",稍后自动重试" : "";
|
|
138
|
+
if (error && !data) return <div className="tc-note">读取改动失败:{error}{retryNote} <button className="tc-btn" onClick={() => setManual((n) => n + 1)}>重试</button></div>;
|
|
139
|
+
if (!data) return <div className="tc-note">读取改动…</div>;
|
|
140
|
+
if (data.error) return <div className="tc-note">{data.error} <button className="tc-btn" onClick={() => setManual((n) => n + 1)}>重试</button></div>;
|
|
141
|
+
const unified = !data.files && files.length > 0;
|
|
142
|
+
if (!unified && data.isolated === false) return <div className="tc-note">{data.reason || "该任务没有独立分支,改动直接写在工作区里(见「文件」)。"}</div>;
|
|
143
|
+
const totalAdd = files.reduce((n, f) => n + f.additions, 0);
|
|
144
|
+
const totalDel = files.reduce((n, f) => n + f.deletions, 0);
|
|
145
|
+
const nothing = files.length === 0;
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className="tc-root">
|
|
149
|
+
<div className="tc-bar">
|
|
150
|
+
{data.branch && <code className="tc-branch">{data.branch}</code>}
|
|
151
|
+
{data.baseLabel && <span className="tc-arrow">→ {data.baseLabel}</span>}
|
|
152
|
+
{!nothing && <span className="tc-stat"><b className="add">+{totalAdd}</b> <b className="del">−{totalDel}</b></span>}
|
|
153
|
+
{data.isDirty && <span className="tc-dirty">● 未提交</span>}
|
|
154
|
+
{(data.ahead ?? 0) > 0 && <span className="tc-ahead">领先 {data.ahead} 个提交</span>}
|
|
155
|
+
<span className="tc-spacer" />
|
|
156
|
+
<button className="tc-btn" onClick={() => setManual((n) => n + 1)} disabled={loading}>{loading ? "…" : "刷新"}</button>
|
|
157
|
+
</div>
|
|
158
|
+
{error && (
|
|
159
|
+
<div className="tc-mergebar bad" role="status">
|
|
160
|
+
⚠ 刷新失败:{error}{loadedAt ? `。下面是 ${clock(loadedAt)} 的结果` : ""}{retryNote}{" "}
|
|
161
|
+
<button className="tc-btn" onClick={() => setManual((n) => n + 1)} disabled={loading}>重试</button>
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
{nothing ? (
|
|
165
|
+
<div className="tc-note">该分支上还没有改动。</div>
|
|
166
|
+
) : (
|
|
167
|
+
<div className="tc-scroll" ref={scrollRef}>
|
|
168
|
+
<div className="tc-list">
|
|
169
|
+
<div className="tc-list-h">{files.length} 个文件有改动</div>
|
|
170
|
+
{files.map((f) => (
|
|
171
|
+
<button key={f.path} className={`tc-frow ${active === f.path ? "on" : ""}`} onClick={() => jump(f.path)} title={f.path}>
|
|
172
|
+
<span className={`tc-st s-${f.status === "?" ? "new" : f.status}`} title={STATUS_LABEL[f.status] || f.status}>{f.status}</span>
|
|
173
|
+
<span className="tc-fpath">{f.path}</span>
|
|
174
|
+
{f.binary ? <span className="tc-bin">bin</span> : f.omitted ? <span className="tc-bin">略</span> : <span className="tc-cnt"><b className="add">+{f.additions}</b> <b className="del">−{f.deletions}</b></span>}
|
|
175
|
+
</button>
|
|
176
|
+
))}
|
|
177
|
+
</div>
|
|
178
|
+
{(data.patchesOmitted ?? 0) > 0 && <div className="tc-empty">改动太多:{data.patchesOmitted} 个文件的 diff 没有带上(只列出文件),可以打开文件查看。</div>}
|
|
179
|
+
{files.slice(0, shown).map((f) => <FileDiff key={f.path} file={f} userToggled={toggled.has(f.path)} onToggle={toggle} refs={secRefs} onOpen={onOpen} />)}
|
|
180
|
+
{files.length > shown && <button className="tc-bigdiff" onClick={() => setShown((n) => n + RENDER_BATCH)}>还有 {(files.length - shown).toLocaleString()} 个文件 —— 再显示 {Math.min(RENDER_BATCH, files.length - shown)} 个</button>}
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|