pi-one-ui 0.1.1
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/LICENSE +21 -0
- package/README.md +106 -0
- package/extensions/app/config/renderer.ts +452 -0
- package/extensions/app/config/shell.ts +2484 -0
- package/extensions/app/ownership.ts +18 -0
- package/extensions/app/panel.ts +533 -0
- package/extensions/app/presets.ts +36 -0
- package/extensions/features/agent-summary/core.ts +194 -0
- package/extensions/features/agent-summary/index.ts +37 -0
- package/extensions/features/compact-thinking.ts +1262 -0
- package/extensions/features/context.ts +857 -0
- package/extensions/features/reference/index.ts +643 -0
- package/extensions/features/reference/session.ts +278 -0
- package/extensions/features/reference/subagent.ts +218 -0
- package/extensions/features/shell/aliases.ts +28 -0
- package/extensions/features/shell/flush-docked-bash.ts +39 -0
- package/extensions/features/shell/startup-header.ts +357 -0
- package/extensions/features/shell/working-message.ts +226 -0
- package/extensions/index.ts +52 -0
- package/extensions/shell/accent-rail-editor.ts +112 -0
- package/extensions/shell/accent-rail-layout-patch.ts +530 -0
- package/extensions/shell/completion-menu.ts +186 -0
- package/extensions/shell/editor-metadata-format.ts +287 -0
- package/extensions/shell/editor-transfer.ts +73 -0
- package/extensions/shell/extension-status.ts +102 -0
- package/extensions/shell/footer-format.ts +388 -0
- package/extensions/shell/footer-layout.ts +144 -0
- package/extensions/shell/footer.ts +1035 -0
- package/extensions/shell/format.ts +577 -0
- package/extensions/shell/git.ts +434 -0
- package/extensions/shell/icons.ts +261 -0
- package/extensions/shell/index.ts +1659 -0
- package/extensions/shell/interaction-summary.ts +1194 -0
- package/extensions/shell/live-context.ts +82 -0
- package/extensions/shell/minimalist-editor.ts +548 -0
- package/extensions/shell/package-version.ts +675 -0
- package/extensions/shell/project-refresh.ts +130 -0
- package/extensions/shell/project-state.ts +60 -0
- package/extensions/shell/prototype-patch-registry.ts +183 -0
- package/extensions/shell/runtime.ts +915 -0
- package/extensions/shell/selector-border.ts +109 -0
- package/extensions/shell/session-lifecycle.ts +60 -0
- package/extensions/shell/settings-command.ts +2144 -0
- package/extensions/shell/settings-previews.ts +137 -0
- package/extensions/shell/state.ts +83 -0
- package/extensions/shell/style.ts +480 -0
- package/extensions/shell/telemetry.ts +90 -0
- package/extensions/shell/ui.ts +1326 -0
- package/extensions/shell/user-message-osc.ts +279 -0
- package/extensions/shell/user-message-styles.ts +266 -0
- package/extensions/shell/user-message.ts +226 -0
- package/extensions/shell/working-line-messages.ts +24 -0
- package/extensions/shell/working-line-spinners.ts +54 -0
- package/extensions/shell/working-line.ts +1603 -0
- package/extensions/tools/ansi-color.ts +44 -0
- package/extensions/tools/ansi-text.ts +52 -0
- package/extensions/tools/component-tree.ts +37 -0
- package/extensions/tools/format.ts +28 -0
- package/extensions/tools/fullscreen-detect.ts +11 -0
- package/extensions/tools/patch-keys.ts +158 -0
- package/extensions/tools/sgr-mouse.ts +59 -0
- package/extensions/tools/tool-loading-icon.ts +20 -0
- package/extensions/tools/tool-result-sanitize.ts +27 -0
- package/extensions/transcript/index.ts +65 -0
- package/extensions/transcript/renderer/compact-mode.ts +1443 -0
- package/extensions/transcript/renderer/default-mode.ts +735 -0
- package/extensions/transcript/renderer/index.ts +286 -0
- package/extensions/transcript/renderer/markdown-enhance.ts +325 -0
- package/extensions/transcript/renderer/mouse/hover.ts +175 -0
- package/extensions/transcript/renderer/mouse/interaction.ts +1016 -0
- package/extensions/transcript/renderer/mouse/layout.ts +207 -0
- package/extensions/transcript/renderer/mouse/packets.ts +96 -0
- package/extensions/transcript/renderer/mouse/scroll.ts +211 -0
- package/extensions/transcript/renderer/tool/diff/ATTRIBUTION.md +28 -0
- package/extensions/transcript/renderer/tool/diff/ansi-utils.ts +128 -0
- package/extensions/transcript/renderer/tool/diff/diff-component.ts +112 -0
- package/extensions/transcript/renderer/tool/diff/diff-edit-render.ts +255 -0
- package/extensions/transcript/renderer/tool/diff/diff-header.ts +184 -0
- package/extensions/transcript/renderer/tool/diff/diff-highlight.ts +131 -0
- package/extensions/transcript/renderer/tool/diff/diff-inline.ts +350 -0
- package/extensions/transcript/renderer/tool/diff/diff-layout.ts +870 -0
- package/extensions/transcript/renderer/tool/diff/diff-limits.ts +189 -0
- package/extensions/transcript/renderer/tool/diff/diff-palette.ts +436 -0
- package/extensions/transcript/renderer/tool/diff/diff-parse.ts +536 -0
- package/extensions/transcript/renderer/tool/diff/diff-presentation.ts +79 -0
- package/extensions/transcript/renderer/tool/diff/diff-renderer.ts +29 -0
- package/extensions/transcript/renderer/tool/diff/diff-text.ts +57 -0
- package/extensions/transcript/renderer/tool/diff/diff-write-render.ts +629 -0
- package/extensions/transcript/renderer/tool/diff/index.ts +147 -0
- package/extensions/transcript/renderer/tool/diff/line-width-safety.ts +100 -0
- package/extensions/transcript/renderer/tool/diff/shiki-highlight.ts +104 -0
- package/extensions/transcript/renderer/tool/diff/write-execution.ts +124 -0
- package/extensions/transcript/renderer/tool/grouping.ts +619 -0
- package/extensions/transcript/renderer/tool/message-display.ts +183 -0
- package/extensions/transcript/renderer/tool/names.ts +195 -0
- package/extensions/transcript/renderer/tool/result.ts +842 -0
- package/extensions/transcript/renderer/tool/show-more-hint.ts +42 -0
- package/extensions/transcript/renderer/transcript-refresh.ts +79 -0
- package/package.json +58 -0
- package/themes/cc-dark.json +91 -0
- package/themes/cc-light.json +91 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { fitToWidth } from "./diff-text.ts";
|
|
3
|
+
|
|
4
|
+
export type DiffLineKind = "add" | "remove" | "context";
|
|
5
|
+
export type DiffEntryKind = "line" | "meta" | "hunk" | "file";
|
|
6
|
+
|
|
7
|
+
export interface DiffLineEntry {
|
|
8
|
+
kind: "line";
|
|
9
|
+
lineKind: DiffLineKind;
|
|
10
|
+
oldLineNumber: number | null;
|
|
11
|
+
newLineNumber: number | null;
|
|
12
|
+
fallbackLineNumber: string;
|
|
13
|
+
content: string;
|
|
14
|
+
hashlineAnchorContent?: string;
|
|
15
|
+
raw: string;
|
|
16
|
+
hunkIndex: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DiffMetaEntry {
|
|
20
|
+
kind: Exclude<DiffEntryKind, "line">;
|
|
21
|
+
raw: string;
|
|
22
|
+
hunkIndex: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ParsedDiffEntry = DiffLineEntry | DiffMetaEntry;
|
|
26
|
+
|
|
27
|
+
export interface ParsedDiff {
|
|
28
|
+
entries: ParsedDiffEntry[];
|
|
29
|
+
stats: DiffStats;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DiffStats {
|
|
33
|
+
added: number;
|
|
34
|
+
removed: number;
|
|
35
|
+
context: number;
|
|
36
|
+
hunks: number;
|
|
37
|
+
files: number;
|
|
38
|
+
lines: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const CANONICAL_LINE_PATTERN = /^([+\- ])(\s*\d+)\|(.*)$/;
|
|
42
|
+
const HASHLINE_ANCHOR_LINE_PATTERN =
|
|
43
|
+
/^([+\- ])(\s*\d+)#([A-Za-z0-9]+| {2}):(.*)$/;
|
|
44
|
+
const LEGACY_LINE_PATTERN = /^([+\- ])(\s*\d+)\s(.*)$/;
|
|
45
|
+
const HUNK_HEADER_PATTERN =
|
|
46
|
+
/^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@(.*)$/;
|
|
47
|
+
const MIN_LINE_NUMBER_WIDTH = 2;
|
|
48
|
+
|
|
49
|
+
function toParsedDiffLine(
|
|
50
|
+
prefix: string,
|
|
51
|
+
lineNumber: string,
|
|
52
|
+
content: string,
|
|
53
|
+
): {
|
|
54
|
+
lineKind: DiffLineKind;
|
|
55
|
+
lineNumber: string;
|
|
56
|
+
content: string;
|
|
57
|
+
} {
|
|
58
|
+
const normalizedLineNumber = lineNumber.trim();
|
|
59
|
+
if (prefix === "+") {
|
|
60
|
+
return { lineKind: "add", lineNumber: normalizedLineNumber, content };
|
|
61
|
+
}
|
|
62
|
+
if (prefix === "-") {
|
|
63
|
+
return { lineKind: "remove", lineNumber: normalizedLineNumber, content };
|
|
64
|
+
}
|
|
65
|
+
return { lineKind: "context", lineNumber: normalizedLineNumber, content };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseCanonicalDiffLine(line: string): {
|
|
69
|
+
lineKind: DiffLineKind;
|
|
70
|
+
lineNumber: string;
|
|
71
|
+
content: string;
|
|
72
|
+
hashlineAnchorContent?: string;
|
|
73
|
+
} | null {
|
|
74
|
+
const hashlineAnchorMatch = line.match(HASHLINE_ANCHOR_LINE_PATTERN);
|
|
75
|
+
if (hashlineAnchorMatch) {
|
|
76
|
+
const lineNumber = hashlineAnchorMatch[2] ?? "";
|
|
77
|
+
const hash = hashlineAnchorMatch[3] ?? "";
|
|
78
|
+
const content = hashlineAnchorMatch[4] ?? "";
|
|
79
|
+
const parsed = toParsedDiffLine(
|
|
80
|
+
hashlineAnchorMatch[1] ?? " ",
|
|
81
|
+
lineNumber,
|
|
82
|
+
content,
|
|
83
|
+
);
|
|
84
|
+
return {
|
|
85
|
+
...parsed,
|
|
86
|
+
hashlineAnchorContent: `${lineNumber.trim()}#${hash}:${content}`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const canonicalMatch = line.match(CANONICAL_LINE_PATTERN);
|
|
91
|
+
const legacyMatch = canonicalMatch ? null : line.match(LEGACY_LINE_PATTERN);
|
|
92
|
+
const matched = canonicalMatch ?? legacyMatch;
|
|
93
|
+
if (!matched) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return toParsedDiffLine(
|
|
98
|
+
matched[1] ?? " ",
|
|
99
|
+
matched[2] ?? "",
|
|
100
|
+
matched[3] ?? "",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function toNumber(value: string | undefined): number | null {
|
|
105
|
+
if (!value) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const parsed = Number.parseInt(value, 10);
|
|
109
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function anchorCanonicalLineCursors(
|
|
113
|
+
kind: DiffLineKind,
|
|
114
|
+
parsedNumber: number | null,
|
|
115
|
+
oldLineCursor: number | null,
|
|
116
|
+
newLineCursor: number | null,
|
|
117
|
+
lineNumberDelta: number,
|
|
118
|
+
): { oldLineCursor: number | null; newLineCursor: number | null } {
|
|
119
|
+
if (parsedNumber === null) {
|
|
120
|
+
return { oldLineCursor, newLineCursor };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (kind === "add") {
|
|
124
|
+
return {
|
|
125
|
+
oldLineCursor,
|
|
126
|
+
newLineCursor: newLineCursor ?? parsedNumber,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
oldLineCursor: parsedNumber,
|
|
132
|
+
newLineCursor: parsedNumber + lineNumberDelta,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function classifyMetaLine(raw: string): DiffMetaEntry["kind"] {
|
|
137
|
+
if (raw.startsWith("@@")) {
|
|
138
|
+
return "hunk";
|
|
139
|
+
}
|
|
140
|
+
if (
|
|
141
|
+
raw.startsWith("diff --git") ||
|
|
142
|
+
raw.startsWith("index ") ||
|
|
143
|
+
raw.startsWith("--- ") ||
|
|
144
|
+
raw.startsWith("+++ ") ||
|
|
145
|
+
raw.startsWith("rename from ") ||
|
|
146
|
+
raw.startsWith("rename to ") ||
|
|
147
|
+
raw.startsWith("new file mode ") ||
|
|
148
|
+
raw.startsWith("deleted file mode ")
|
|
149
|
+
) {
|
|
150
|
+
return "file";
|
|
151
|
+
}
|
|
152
|
+
return "meta";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function pushParsedLineEntry(
|
|
156
|
+
entries: ParsedDiffEntry[],
|
|
157
|
+
lineKind: DiffLineKind,
|
|
158
|
+
oldLineNumber: number | null,
|
|
159
|
+
newLineNumber: number | null,
|
|
160
|
+
fallbackLineNumber: string,
|
|
161
|
+
rawLine: string,
|
|
162
|
+
hunkIndex: number,
|
|
163
|
+
): void {
|
|
164
|
+
entries.push({
|
|
165
|
+
kind: "line",
|
|
166
|
+
lineKind,
|
|
167
|
+
oldLineNumber,
|
|
168
|
+
newLineNumber,
|
|
169
|
+
fallbackLineNumber,
|
|
170
|
+
content: rawLine.slice(1),
|
|
171
|
+
raw: rawLine,
|
|
172
|
+
hunkIndex,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function createMetaEntry(raw: string, hunkIndex: number): DiffMetaEntry {
|
|
177
|
+
return {
|
|
178
|
+
kind: classifyMetaLine(raw),
|
|
179
|
+
raw,
|
|
180
|
+
hunkIndex,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function ensureImplicitHunk(currentHunk: number): number {
|
|
185
|
+
return currentHunk > 0 ? currentHunk : 1;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function parseDiff(diffText: string): ParsedDiff {
|
|
189
|
+
const stats: DiffStats = {
|
|
190
|
+
added: 0,
|
|
191
|
+
removed: 0,
|
|
192
|
+
context: 0,
|
|
193
|
+
hunks: 0,
|
|
194
|
+
files: 0,
|
|
195
|
+
lines: 0,
|
|
196
|
+
};
|
|
197
|
+
const entries: ParsedDiffEntry[] = [];
|
|
198
|
+
|
|
199
|
+
if (!diffText.trim()) {
|
|
200
|
+
return { entries, stats };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let hunkIndex = 0;
|
|
204
|
+
let oldLineCursor: number | null = null;
|
|
205
|
+
let newLineCursor: number | null = null;
|
|
206
|
+
let lineNumberDelta = 0;
|
|
207
|
+
|
|
208
|
+
for (const rawLine of diffText.replace(/\r/g, "").split("\n")) {
|
|
209
|
+
stats.lines++;
|
|
210
|
+
|
|
211
|
+
const hunkMatch = rawLine.match(HUNK_HEADER_PATTERN);
|
|
212
|
+
if (hunkMatch) {
|
|
213
|
+
hunkIndex++;
|
|
214
|
+
stats.hunks = Math.max(stats.hunks, hunkIndex);
|
|
215
|
+
oldLineCursor = toNumber(hunkMatch[1]);
|
|
216
|
+
newLineCursor = toNumber(hunkMatch[3]);
|
|
217
|
+
lineNumberDelta = (newLineCursor ?? 0) - (oldLineCursor ?? 0);
|
|
218
|
+
entries.push({ kind: "hunk", raw: rawLine, hunkIndex });
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (rawLine.startsWith("diff --git ")) {
|
|
223
|
+
stats.files++;
|
|
224
|
+
oldLineCursor = null;
|
|
225
|
+
newLineCursor = null;
|
|
226
|
+
lineNumberDelta = 0;
|
|
227
|
+
entries.push({ kind: "file", raw: rawLine, hunkIndex });
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (rawLine.startsWith("--- ") || rawLine.startsWith("+++ ")) {
|
|
232
|
+
oldLineCursor = null;
|
|
233
|
+
newLineCursor = null;
|
|
234
|
+
lineNumberDelta = 0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const canonical = parseCanonicalDiffLine(rawLine);
|
|
238
|
+
if (canonical) {
|
|
239
|
+
hunkIndex = ensureImplicitHunk(hunkIndex);
|
|
240
|
+
stats.hunks = Math.max(stats.hunks, hunkIndex);
|
|
241
|
+
|
|
242
|
+
const parsedNumber = toNumber(canonical.lineNumber);
|
|
243
|
+
const anchoredCursors = anchorCanonicalLineCursors(
|
|
244
|
+
canonical.lineKind,
|
|
245
|
+
parsedNumber,
|
|
246
|
+
oldLineCursor,
|
|
247
|
+
newLineCursor,
|
|
248
|
+
lineNumberDelta,
|
|
249
|
+
);
|
|
250
|
+
oldLineCursor = anchoredCursors.oldLineCursor;
|
|
251
|
+
newLineCursor = anchoredCursors.newLineCursor;
|
|
252
|
+
|
|
253
|
+
const oldLineNumber = canonical.lineKind === "add" ? null : oldLineCursor;
|
|
254
|
+
const newLineNumber =
|
|
255
|
+
canonical.lineKind === "remove" ? null : newLineCursor;
|
|
256
|
+
|
|
257
|
+
if (canonical.lineKind === "add") {
|
|
258
|
+
stats.added++;
|
|
259
|
+
if (newLineCursor !== null) {
|
|
260
|
+
newLineCursor++;
|
|
261
|
+
}
|
|
262
|
+
lineNumberDelta++;
|
|
263
|
+
} else if (canonical.lineKind === "remove") {
|
|
264
|
+
stats.removed++;
|
|
265
|
+
if (oldLineCursor !== null) {
|
|
266
|
+
oldLineCursor++;
|
|
267
|
+
}
|
|
268
|
+
lineNumberDelta--;
|
|
269
|
+
} else {
|
|
270
|
+
stats.context++;
|
|
271
|
+
if (oldLineCursor !== null) {
|
|
272
|
+
oldLineCursor++;
|
|
273
|
+
}
|
|
274
|
+
if (newLineCursor !== null) {
|
|
275
|
+
newLineCursor++;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
entries.push({
|
|
280
|
+
kind: "line",
|
|
281
|
+
lineKind: canonical.lineKind,
|
|
282
|
+
oldLineNumber,
|
|
283
|
+
newLineNumber,
|
|
284
|
+
fallbackLineNumber: canonical.lineNumber,
|
|
285
|
+
content: canonical.content,
|
|
286
|
+
hashlineAnchorContent: canonical.hashlineAnchorContent,
|
|
287
|
+
raw: rawLine,
|
|
288
|
+
hunkIndex,
|
|
289
|
+
});
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (rawLine.startsWith("-") && !rawLine.startsWith("---")) {
|
|
294
|
+
hunkIndex = ensureImplicitHunk(hunkIndex);
|
|
295
|
+
stats.hunks = Math.max(stats.hunks, hunkIndex);
|
|
296
|
+
stats.removed++;
|
|
297
|
+
const oldLineNumber = oldLineCursor;
|
|
298
|
+
if (oldLineCursor !== null) {
|
|
299
|
+
oldLineCursor++;
|
|
300
|
+
}
|
|
301
|
+
lineNumberDelta--;
|
|
302
|
+
pushParsedLineEntry(
|
|
303
|
+
entries,
|
|
304
|
+
"remove",
|
|
305
|
+
oldLineNumber,
|
|
306
|
+
null,
|
|
307
|
+
oldLineNumber !== null ? `${oldLineNumber}` : "",
|
|
308
|
+
rawLine,
|
|
309
|
+
hunkIndex,
|
|
310
|
+
);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (rawLine.startsWith("+") && !rawLine.startsWith("+++")) {
|
|
315
|
+
hunkIndex = ensureImplicitHunk(hunkIndex);
|
|
316
|
+
stats.hunks = Math.max(stats.hunks, hunkIndex);
|
|
317
|
+
stats.added++;
|
|
318
|
+
const newLineNumber = newLineCursor;
|
|
319
|
+
if (newLineCursor !== null) {
|
|
320
|
+
newLineCursor++;
|
|
321
|
+
}
|
|
322
|
+
lineNumberDelta++;
|
|
323
|
+
pushParsedLineEntry(
|
|
324
|
+
entries,
|
|
325
|
+
"add",
|
|
326
|
+
null,
|
|
327
|
+
newLineNumber,
|
|
328
|
+
newLineNumber !== null ? `${newLineNumber}` : "",
|
|
329
|
+
rawLine,
|
|
330
|
+
hunkIndex,
|
|
331
|
+
);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (rawLine.startsWith(" ")) {
|
|
336
|
+
hunkIndex = ensureImplicitHunk(hunkIndex);
|
|
337
|
+
stats.hunks = Math.max(stats.hunks, hunkIndex);
|
|
338
|
+
stats.context++;
|
|
339
|
+
const oldLineNumber = oldLineCursor;
|
|
340
|
+
const newLineNumber = newLineCursor;
|
|
341
|
+
if (oldLineCursor !== null) {
|
|
342
|
+
oldLineCursor++;
|
|
343
|
+
}
|
|
344
|
+
if (newLineCursor !== null) {
|
|
345
|
+
newLineCursor++;
|
|
346
|
+
}
|
|
347
|
+
pushParsedLineEntry(
|
|
348
|
+
entries,
|
|
349
|
+
"context",
|
|
350
|
+
oldLineNumber,
|
|
351
|
+
newLineNumber,
|
|
352
|
+
oldLineNumber !== null
|
|
353
|
+
? `${oldLineNumber}`
|
|
354
|
+
: newLineNumber !== null
|
|
355
|
+
? `${newLineNumber}`
|
|
356
|
+
: "",
|
|
357
|
+
rawLine,
|
|
358
|
+
hunkIndex,
|
|
359
|
+
);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
entries.push(createMetaEntry(rawLine, hunkIndex));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (
|
|
367
|
+
stats.hunks === 0 &&
|
|
368
|
+
(stats.added > 0 || stats.removed > 0 || stats.context > 0)
|
|
369
|
+
) {
|
|
370
|
+
stats.hunks = 1;
|
|
371
|
+
}
|
|
372
|
+
if (stats.files === 0) {
|
|
373
|
+
const patchStyleFileHeaders = entries.filter(
|
|
374
|
+
(entry) => entry.kind === "file" && entry.raw.startsWith("+++ "),
|
|
375
|
+
).length;
|
|
376
|
+
if (patchStyleFileHeaders > 0) {
|
|
377
|
+
stats.files = patchStyleFileHeaders;
|
|
378
|
+
} else if (stats.hunks > 0) {
|
|
379
|
+
stats.files = 1;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return { entries, stats };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function getHashlineAnchorLabel(
|
|
387
|
+
entry: DiffLineEntry,
|
|
388
|
+
): string | undefined {
|
|
389
|
+
if (!entry.hashlineAnchorContent) {
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
const separatorIndex = entry.hashlineAnchorContent.indexOf(":");
|
|
393
|
+
return separatorIndex >= 0
|
|
394
|
+
? entry.hashlineAnchorContent.slice(0, separatorIndex)
|
|
395
|
+
: entry.hashlineAnchorContent;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function getLineNumberWidth(
|
|
399
|
+
entries: ParsedDiffEntry[],
|
|
400
|
+
showHashlineAnchors = false,
|
|
401
|
+
): number {
|
|
402
|
+
let maxWidth = MIN_LINE_NUMBER_WIDTH;
|
|
403
|
+
|
|
404
|
+
for (const entry of entries) {
|
|
405
|
+
if (entry.kind !== "line") {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (showHashlineAnchors) {
|
|
410
|
+
const anchorLabel = getHashlineAnchorLabel(entry);
|
|
411
|
+
if (anchorLabel) {
|
|
412
|
+
maxWidth = Math.max(maxWidth, visibleWidth(anchorLabel));
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const candidates = [
|
|
418
|
+
entry.oldLineNumber,
|
|
419
|
+
entry.newLineNumber,
|
|
420
|
+
toNumber(entry.fallbackLineNumber),
|
|
421
|
+
].filter((value): value is number => value !== null);
|
|
422
|
+
|
|
423
|
+
for (const candidate of candidates) {
|
|
424
|
+
const digits = `${candidate}`.length;
|
|
425
|
+
if (digits > maxWidth) {
|
|
426
|
+
maxWidth = digits;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return maxWidth;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function formatLineNumber(
|
|
435
|
+
value: number | null,
|
|
436
|
+
fallback: string,
|
|
437
|
+
width: number,
|
|
438
|
+
): string {
|
|
439
|
+
if (value !== null) {
|
|
440
|
+
return `${value}`.padStart(width, " ");
|
|
441
|
+
}
|
|
442
|
+
if (fallback.trim()) {
|
|
443
|
+
return fallback.trim().slice(-width).padStart(width, " ");
|
|
444
|
+
}
|
|
445
|
+
return " ".repeat(width);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function formatLineNumberLabel(
|
|
449
|
+
entry: DiffLineEntry,
|
|
450
|
+
value: number | null,
|
|
451
|
+
fallback: string,
|
|
452
|
+
width: number,
|
|
453
|
+
showHashlineAnchors: boolean,
|
|
454
|
+
): string {
|
|
455
|
+
const anchorLabel = showHashlineAnchors
|
|
456
|
+
? getHashlineAnchorLabel(entry)
|
|
457
|
+
: undefined;
|
|
458
|
+
if (anchorLabel) {
|
|
459
|
+
return fitToWidth(anchorLabel, width);
|
|
460
|
+
}
|
|
461
|
+
return formatLineNumber(value, fallback, width);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function getCompactLineRenderContent(
|
|
465
|
+
entry: DiffLineEntry,
|
|
466
|
+
showHashlineAnchors: boolean,
|
|
467
|
+
): string {
|
|
468
|
+
return showHashlineAnchors && entry.hashlineAnchorContent
|
|
469
|
+
? entry.hashlineAnchorContent
|
|
470
|
+
: entry.content;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function countDiffLineEntries(
|
|
474
|
+
entries: readonly ParsedDiffEntry[],
|
|
475
|
+
): number {
|
|
476
|
+
let count = 0;
|
|
477
|
+
for (const entry of entries) {
|
|
478
|
+
if (entry.kind === "line") count++;
|
|
479
|
+
}
|
|
480
|
+
return count;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export function collectDiffStats(
|
|
484
|
+
entries: ParsedDiffEntry[],
|
|
485
|
+
fallbackHunks = 0,
|
|
486
|
+
fallbackFiles = 0,
|
|
487
|
+
): DiffStats {
|
|
488
|
+
const stats: DiffStats = {
|
|
489
|
+
added: 0,
|
|
490
|
+
removed: 0,
|
|
491
|
+
context: 0,
|
|
492
|
+
hunks: fallbackHunks,
|
|
493
|
+
files: fallbackFiles,
|
|
494
|
+
lines: entries.length,
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
const hunkIndexes = new Set<number>();
|
|
498
|
+
let explicitFileCount = 0;
|
|
499
|
+
|
|
500
|
+
for (const entry of entries) {
|
|
501
|
+
if (entry.kind === "line") {
|
|
502
|
+
if (entry.lineKind === "add") {
|
|
503
|
+
stats.added++;
|
|
504
|
+
} else if (entry.lineKind === "remove") {
|
|
505
|
+
stats.removed++;
|
|
506
|
+
} else {
|
|
507
|
+
stats.context++;
|
|
508
|
+
}
|
|
509
|
+
if (entry.hunkIndex > 0) {
|
|
510
|
+
hunkIndexes.add(entry.hunkIndex);
|
|
511
|
+
}
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (entry.kind === "hunk" && entry.hunkIndex > 0) {
|
|
516
|
+
hunkIndexes.add(entry.hunkIndex);
|
|
517
|
+
}
|
|
518
|
+
if (entry.kind === "file") {
|
|
519
|
+
explicitFileCount++;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (hunkIndexes.size > 0) {
|
|
524
|
+
stats.hunks = Math.max(stats.hunks, hunkIndexes.size);
|
|
525
|
+
}
|
|
526
|
+
if (explicitFileCount > 0) {
|
|
527
|
+
stats.files = Math.max(stats.files, explicitFileCount);
|
|
528
|
+
} else if (entries.length > 0) {
|
|
529
|
+
stats.files = Math.max(stats.files, 1);
|
|
530
|
+
}
|
|
531
|
+
if (stats.hunks === 0 && entries.some((entry) => entry.kind === "line")) {
|
|
532
|
+
stats.hunks = 1;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return stats;
|
|
536
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { pluralize } from "./diff-text.ts";
|
|
3
|
+
import type { ToolDisplayConfig } from "../../../../app/config/renderer.ts";
|
|
4
|
+
|
|
5
|
+
export interface DiffSummaryStats {
|
|
6
|
+
added: number;
|
|
7
|
+
removed: number;
|
|
8
|
+
hunks: number;
|
|
9
|
+
files: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type DiffPresentationMode = "split" | "unified" | "compact" | "summary";
|
|
13
|
+
|
|
14
|
+
const MIN_COMPACT_DIFF_WIDTH = 8;
|
|
15
|
+
const MIN_UNIFIED_DIFF_WIDTH = 18;
|
|
16
|
+
|
|
17
|
+
export function normalizeDiffRenderWidth(width: number): number {
|
|
18
|
+
if (!Number.isFinite(width)) {
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
return Math.max(0, Math.floor(width));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveDiffPresentationMode(
|
|
25
|
+
config: Pick<ToolDisplayConfig, "diffViewMode" | "diffSplitMinWidth">,
|
|
26
|
+
width: number,
|
|
27
|
+
canRenderSplitLayout: boolean,
|
|
28
|
+
): DiffPresentationMode {
|
|
29
|
+
const safeWidth = normalizeDiffRenderWidth(width);
|
|
30
|
+
if (safeWidth < MIN_COMPACT_DIFF_WIDTH) {
|
|
31
|
+
return "summary";
|
|
32
|
+
}
|
|
33
|
+
if (safeWidth < MIN_UNIFIED_DIFF_WIDTH) {
|
|
34
|
+
return "compact";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
switch (config.diffViewMode) {
|
|
38
|
+
case "split":
|
|
39
|
+
return canRenderSplitLayout ? "split" : "unified";
|
|
40
|
+
case "unified":
|
|
41
|
+
return "unified";
|
|
42
|
+
case "auto":
|
|
43
|
+
default:
|
|
44
|
+
return safeWidth >= config.diffSplitMinWidth && canRenderSplitLayout
|
|
45
|
+
? "split"
|
|
46
|
+
: "unified";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildDiffSummaryText(
|
|
51
|
+
stats: DiffSummaryStats,
|
|
52
|
+
width: number,
|
|
53
|
+
): string {
|
|
54
|
+
const safeWidth = normalizeDiffRenderWidth(width);
|
|
55
|
+
if (safeWidth === 0) {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const summaryCandidates = [
|
|
60
|
+
`↳ diff +${stats.added} -${stats.removed} • ${stats.hunks} ${pluralize(stats.hunks, "hunk")} • ${stats.files} ${pluralize(stats.files, "file")}`,
|
|
61
|
+
`↳ diff +${stats.added} -${stats.removed} • ${stats.hunks}h • ${stats.files}f`,
|
|
62
|
+
`↳ diff +${stats.added} -${stats.removed}`,
|
|
63
|
+
`+${stats.added} -${stats.removed}`,
|
|
64
|
+
"diff",
|
|
65
|
+
"…",
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
for (const candidate of summaryCandidates) {
|
|
69
|
+
if (visibleWidth(candidate) <= safeWidth) {
|
|
70
|
+
return candidate;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return truncateToWidth(
|
|
75
|
+
summaryCandidates[summaryCandidates.length - 1] ?? "",
|
|
76
|
+
safeWidth,
|
|
77
|
+
"",
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry surface for the diff renderer.
|
|
3
|
+
*
|
|
4
|
+
* Implementation is split across focused modules by concern:
|
|
5
|
+
* - diff-parse.ts : diff string → line/meta/hunk/file entries + stats
|
|
6
|
+
* - diff-inline.ts : inline token diff (LCS spans) + split-row pairing
|
|
7
|
+
* - diff-palette.ts : theme/color/SGR (row backgrounds, inline emphasis)
|
|
8
|
+
* - diff-highlight.ts : syntax highlighting (sync + shiki)
|
|
9
|
+
* - diff-layout.ts : unified / split / compact layout
|
|
10
|
+
* - diff-header.ts : file/hunk headers + stat/summary/frame lines
|
|
11
|
+
* - diff-limits.ts : collapse/expand limits + width clamping
|
|
12
|
+
* - diff-component.ts : shared component cache / live-config plumbing
|
|
13
|
+
* - diff-edit-render.ts : edit diff result rendering + stats
|
|
14
|
+
* - diff-write-render.ts : write diff result rendering + stats
|
|
15
|
+
*
|
|
16
|
+
* This file re-exports the stable public symbols so external importers
|
|
17
|
+
* (diff/index.ts, compact-mode.ts, tests) keep working unchanged.
|
|
18
|
+
*/
|
|
19
|
+
export { shouldHighlightCodeBlock } from "./diff-highlight.ts";
|
|
20
|
+
export type { DisplayConfigInput } from "./diff-component.ts";
|
|
21
|
+
export { isRichDiffComponent } from "./diff-component.ts";
|
|
22
|
+
export {
|
|
23
|
+
countEditDiffStats,
|
|
24
|
+
renderEditDiffResult,
|
|
25
|
+
} from "./diff-edit-render.ts";
|
|
26
|
+
export {
|
|
27
|
+
countWriteDiffStats,
|
|
28
|
+
renderWriteDiffResult,
|
|
29
|
+
} from "./diff-write-render.ts";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
truncateToWidth,
|
|
3
|
+
visibleWidth,
|
|
4
|
+
wrapTextWithAnsi,
|
|
5
|
+
} from "@earendil-works/pi-tui";
|
|
6
|
+
|
|
7
|
+
export function pluralize(
|
|
8
|
+
count: number,
|
|
9
|
+
singular: string,
|
|
10
|
+
plural = `${singular}s`,
|
|
11
|
+
): string {
|
|
12
|
+
return count === 1 ? singular : plural;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function splitWriteContentLines(content: string): string[] {
|
|
16
|
+
if (!content) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const normalized = content.replace(/\r/g, "");
|
|
21
|
+
const lines = normalized.split("\n");
|
|
22
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
23
|
+
lines.pop();
|
|
24
|
+
}
|
|
25
|
+
return lines;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function normalizeCodeWhitespace(text: string): string {
|
|
29
|
+
return text.replace(/\t/g, " ");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function fitToWidth(text: string, width: number): string {
|
|
33
|
+
const trimmed = truncateToWidth(text, width, "");
|
|
34
|
+
const gap = Math.max(0, width - visibleWidth(trimmed));
|
|
35
|
+
return gap > 0 ? `${trimmed}${" ".repeat(gap)}` : trimmed;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function wrapToWidth(
|
|
39
|
+
text: string,
|
|
40
|
+
width: number,
|
|
41
|
+
wordWrap: boolean,
|
|
42
|
+
): string[] {
|
|
43
|
+
if (width <= 0) {
|
|
44
|
+
return [""];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!wordWrap) {
|
|
48
|
+
return [fitToWidth(text, width)];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const wrapped = wrapTextWithAnsi(text, width);
|
|
52
|
+
if (wrapped.length === 0) {
|
|
53
|
+
return [fitToWidth("", width)];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return wrapped.map((line) => fitToWidth(line, width));
|
|
57
|
+
}
|