pi-cc-extensions 0.8.69 → 0.8.71
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/extensions/renderer/compact-mode.ts +5 -5
- package/extensions/renderer/default-mode.ts +4 -4
- package/extensions/renderer/mouse/interaction.ts +6 -3
- package/extensions/renderer/mouse/scroll.ts +23 -1
- package/extensions/renderer/tool/diff/diff-edit-render.ts +27 -7
- package/extensions/renderer/tool/diff/diff-inline.ts +13 -1
- package/extensions/renderer/tool/diff/diff-layout.ts +37 -5
- package/extensions/renderer/tool/diff/diff-parse.ts +36 -6
- package/extensions/renderer/tool/grouping.ts +16 -5
- package/extensions/renderer/tool/names.ts +111 -10
- package/extensions/utils/patch-keys.ts +1 -0
- package/package.json +1 -1
|
@@ -36,8 +36,8 @@ import { renderRichToolResult } from "./tool/diff/index.ts";
|
|
|
36
36
|
import type { WriteExecutionMetadataStore } from "./tool/diff/write-execution.ts";
|
|
37
37
|
import { isToolCallHovered } from "./mouse/hover.ts";
|
|
38
38
|
import { insetComponent, renderExpandedToolResult, scheduleAnimation } from "./tool/result.ts";
|
|
39
|
-
import { oneLine } from "../utils/format.ts";
|
|
40
39
|
import { paddedBackgroundRow } from "./tool/grouping.ts";
|
|
40
|
+
import { formatDisplayPath } from "./tool/names.ts";
|
|
41
41
|
import { hasVisibleText, stripBackgroundAnsi } from "../utils/ansi-text.ts";
|
|
42
42
|
import { walkComponentTree } from "../utils/component-tree.ts";
|
|
43
43
|
import {
|
|
@@ -502,13 +502,12 @@ function compactEditWriteLine(
|
|
|
502
502
|
const theme = themeOf();
|
|
503
503
|
const name = String(component.toolName ?? "tool");
|
|
504
504
|
const args = component.args ?? {};
|
|
505
|
-
const path =
|
|
505
|
+
const path =
|
|
506
506
|
typeof args.path === "string" && args.path
|
|
507
507
|
? args.path
|
|
508
508
|
: typeof args.file_path === "string" && args.file_path
|
|
509
509
|
? args.file_path
|
|
510
|
-
: ""
|
|
511
|
-
);
|
|
510
|
+
: "";
|
|
512
511
|
const isError = component.result?.isError === true;
|
|
513
512
|
const isPending = !component.result || component.isPartial === true;
|
|
514
513
|
const icon = isError ? "✗" : isPending ? toolLoadingIcon() : "✓";
|
|
@@ -542,7 +541,8 @@ function compactEditWriteLine(
|
|
|
542
541
|
visibleWidth(statsText) +
|
|
543
542
|
visibleWidth(hintText);
|
|
544
543
|
const pathWidth = Math.max(0, width - fixedWidth - (path ? 1 : 0));
|
|
545
|
-
const pathPart =
|
|
544
|
+
const pathPart =
|
|
545
|
+
pathWidth > 0 && path ? ` ${formatDisplayPath(path, component.cwd, pathWidth)}` : "";
|
|
546
546
|
const line = `${iconPart}${namePart}${theme.fg("toolTitle", pathPart)}${statsStyled}${hintText ? theme.fg("dim", hintText) : ""}`;
|
|
547
547
|
return ["", truncateToWidth(line, width, "")];
|
|
548
548
|
}
|
|
@@ -17,7 +17,6 @@ import { isToolCallHovered } from "./mouse/hover.ts";
|
|
|
17
17
|
import {
|
|
18
18
|
countLines,
|
|
19
19
|
hasExpandableDetail,
|
|
20
|
-
headTruncateToWidth,
|
|
21
20
|
insetComponent,
|
|
22
21
|
isToolExpanded,
|
|
23
22
|
outputLineCount,
|
|
@@ -37,7 +36,7 @@ import { showMoreHintText } from "./tool/show-more-hint.ts";
|
|
|
37
36
|
import { countWriteDiffStats } from "./tool/diff/diff-renderer.ts";
|
|
38
37
|
import { renderRichToolResult, type WriteExecutionMetadataStore } from "./tool/diff/index.ts";
|
|
39
38
|
import { getMessageDisplayTheme } from "./tool/message-display.ts";
|
|
40
|
-
import { humanizeToolLabel, toolCallSummary } from "./tool/names.ts";
|
|
39
|
+
import { fitToolCallSummary, humanizeToolLabel, toolCallSummary } from "./tool/names.ts";
|
|
41
40
|
|
|
42
41
|
// 成功勾:亮绿 truecolor(与 message-display 一致)
|
|
43
42
|
const BRIGHT_GREEN = "\x1b[38;2;80;220;100m";
|
|
@@ -235,6 +234,7 @@ function createCcstyleTool(
|
|
|
235
234
|
const summary = toolCallSummary(toolName, args, {
|
|
236
235
|
title: label === toolName ? humanizeToolLabel(label) : label,
|
|
237
236
|
variant: "default",
|
|
237
|
+
cwd: context?.cwd,
|
|
238
238
|
});
|
|
239
239
|
let writeStatsText = "";
|
|
240
240
|
let writeStatsStyled = "";
|
|
@@ -267,8 +267,8 @@ function createCcstyleTool(
|
|
|
267
267
|
);
|
|
268
268
|
const mainWidth = Math.max(0, callWidth - visibleWidth(extraText));
|
|
269
269
|
cachedWidth = width;
|
|
270
|
-
//
|
|
271
|
-
cachedLine = `${lead}${icon} ${theme.fg("toolTitle",
|
|
270
|
+
// 路径按最终可用宽度中间截断,避免整行二次截断隐藏文件名。
|
|
271
|
+
cachedLine = `${lead}${icon} ${theme.fg("toolTitle", fitToolCallSummary(summary, mainWidth))}${extraStyled}`;
|
|
272
272
|
return [truncateToWidth(cachedLine, viewportWidth, "")];
|
|
273
273
|
},
|
|
274
274
|
invalidate() {},
|
|
@@ -37,9 +37,10 @@ import {
|
|
|
37
37
|
isScrollbarColumnAt,
|
|
38
38
|
} from "./layout.ts";
|
|
39
39
|
import {
|
|
40
|
-
disableOfficialScrollToEnd,
|
|
41
40
|
fullscreenLazyTui,
|
|
42
41
|
hideScrollButton,
|
|
42
|
+
restoreOfficialScrollToEnd,
|
|
43
|
+
syncOfficialScrollToEnd,
|
|
43
44
|
isScrollBottomInput,
|
|
44
45
|
renderScrollButton,
|
|
45
46
|
resetScrollButtonState,
|
|
@@ -878,6 +879,7 @@ export function teardownToolMouseInteraction(
|
|
|
878
879
|
}
|
|
879
880
|
restoreToolMouseRenderPatch();
|
|
880
881
|
restoreFullscreenViewportInput(getToolMouseTui());
|
|
882
|
+
restoreOfficialScrollToEnd(getToolMouseTui());
|
|
881
883
|
resetScrollButtonState();
|
|
882
884
|
setToolMouseTui(null);
|
|
883
885
|
toolMouseUi = null;
|
|
@@ -893,6 +895,7 @@ export function resetToolHoverState(): void {
|
|
|
893
895
|
setHoveredCompactAssistant(null);
|
|
894
896
|
setScrollButtonVisible(false);
|
|
895
897
|
setScrollButtonHovered(false);
|
|
898
|
+
restoreOfficialScrollToEnd(getToolMouseTui());
|
|
896
899
|
releaseFullscreenToolMouseMotion(getToolMouseTui());
|
|
897
900
|
}
|
|
898
901
|
|
|
@@ -917,12 +920,12 @@ export function installToolMouseInteraction(
|
|
|
917
920
|
if (isLazyProxyTui(tui)) {
|
|
918
921
|
patchFullscreenViewportInput(tui);
|
|
919
922
|
ensureFullscreenToolMouseMotion(tui);
|
|
920
|
-
|
|
923
|
+
syncOfficialScrollToEnd(tui);
|
|
921
924
|
setScrollButtonWidget({
|
|
922
925
|
render: (width: number) => {
|
|
923
926
|
patchFullscreenViewportInput(tui);
|
|
924
927
|
ensureFullscreenToolMouseMotion(tui);
|
|
925
|
-
|
|
928
|
+
syncOfficialScrollToEnd(tui);
|
|
926
929
|
return renderScrollButton(width, theme);
|
|
927
930
|
},
|
|
928
931
|
invalidate() {},
|
|
@@ -9,6 +9,7 @@ import { config } from "../../config/config.ts";
|
|
|
9
9
|
import { isLazyProxyTui } from "../../utils/fullscreen-detect.ts";
|
|
10
10
|
import { parseSgrMousePackets } from "./packets.ts";
|
|
11
11
|
import {
|
|
12
|
+
OFFICIAL_SCROLL_TO_END_KEY,
|
|
12
13
|
patchRegistry,
|
|
13
14
|
SCROLL_BUTTON_STATE_SLOT,
|
|
14
15
|
TOOL_MOUSE_TUI_SLOT,
|
|
@@ -81,7 +82,28 @@ export function fullscreenLazyTui(tui: any): boolean {
|
|
|
81
82
|
|
|
82
83
|
/** 关掉 pi 0.85 Jump to latest overlay,避免和本仓库 dock 按钮叠两层。 */
|
|
83
84
|
export function disableOfficialScrollToEnd(tui: any): void {
|
|
84
|
-
if (
|
|
85
|
+
if (!tui) return;
|
|
86
|
+
const current = tui.scrollToEndIndicator;
|
|
87
|
+
if (typeof current !== "function") return;
|
|
88
|
+
// 原函数放 object 里,避免惰性 Proxy 对 function 属性再包一层。
|
|
89
|
+
if (!tui[OFFICIAL_SCROLL_TO_END_KEY]) {
|
|
90
|
+
tui[OFFICIAL_SCROLL_TO_END_KEY] = { original: current };
|
|
91
|
+
}
|
|
92
|
+
tui.scrollToEndIndicator = undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** /ccstyle off 或 teardown 时还回官方 overlay。 */
|
|
96
|
+
export function restoreOfficialScrollToEnd(tui: any): void {
|
|
97
|
+
if (!tui) return;
|
|
98
|
+
const original = tui[OFFICIAL_SCROLL_TO_END_KEY]?.original;
|
|
99
|
+
if (typeof original === "function") tui.scrollToEndIndicator = original;
|
|
100
|
+
tui[OFFICIAL_SCROLL_TO_END_KEY] = undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** on/compact 用 dock 按钮;off 还回 pi 原生 Jump to latest。 */
|
|
104
|
+
export function syncOfficialScrollToEnd(tui: any): void {
|
|
105
|
+
if (toolMouseInteractionActive()) disableOfficialScrollToEnd(tui);
|
|
106
|
+
else restoreOfficialScrollToEnd(tui);
|
|
85
107
|
}
|
|
86
108
|
|
|
87
109
|
/** 官方 fullscreen:是否已跟随 transcript 底部(按钮隐藏条件)。 */
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { Text, type Component } from "@earendil-works/pi-tui";
|
|
2
2
|
import type { EditToolDetails } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { sanitizeToolResultText } from "../../../utils/tool-result-sanitize.ts";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getLineNumberWidth,
|
|
6
|
+
parseDiff,
|
|
7
|
+
type ParsedDiff,
|
|
8
|
+
type ParsedDiffEntry,
|
|
9
|
+
} from "./diff-parse.ts";
|
|
5
10
|
import { buildInlineHighlightMap, buildSplitRows } from "./diff-inline.ts";
|
|
6
11
|
import { resolveDiffPalette, type DiffTheme } from "./diff-palette.ts";
|
|
7
12
|
import { createCodeLineHighlighter, resolveLanguageFromPath } from "./diff-highlight.ts";
|
|
@@ -44,6 +49,20 @@ function safeGetDiff(details: unknown): string {
|
|
|
44
49
|
return typeof typed.diff === "string" ? typed.diff : "";
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
// Pi appends an omission marker after final context to say the file continues.
|
|
53
|
+
// It does not separate visible diff regions, so keep only omissions before a rendered line.
|
|
54
|
+
function omitTerminalOmissions(entries: ParsedDiffEntry[]): ParsedDiffEntry[] {
|
|
55
|
+
let lastLineIndex = -1;
|
|
56
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
57
|
+
if (entries[index]?.kind === "line") {
|
|
58
|
+
lastLineIndex = index;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return entries.filter((entry, index) => entry.kind !== "omission" || index < lastLineIndex);
|
|
64
|
+
}
|
|
65
|
+
|
|
47
66
|
/**
|
|
48
67
|
* edit 变更行统计(compact 单行 `(+A -D)` 用):复用现有 parser,diff 文件头不计入。
|
|
49
68
|
* 无 diff 或解析失败时返回 undefined,不把未知状态误报为零变更。
|
|
@@ -85,22 +104,23 @@ export function renderEditDiffResult(
|
|
|
85
104
|
return new Text(theme.fg("warning", `↳ unable to render diff: ${message}`), 0, 0);
|
|
86
105
|
}
|
|
87
106
|
|
|
88
|
-
|
|
107
|
+
const renderEntries = omitTerminalOmissions(parsed.entries);
|
|
108
|
+
if (renderEntries.length === 0) {
|
|
89
109
|
return new Text(theme.fg("muted", "↳ no diff data"), 0, 0);
|
|
90
110
|
}
|
|
91
111
|
|
|
92
|
-
const splitRows = buildSplitRows(
|
|
112
|
+
const splitRows = buildSplitRows(renderEntries);
|
|
93
113
|
const showHashlineAnchors =
|
|
94
114
|
options.expanded === true &&
|
|
95
|
-
|
|
96
|
-
const lineNumberWidth = getLineNumberWidth(
|
|
115
|
+
renderEntries.some((entry) => entry.kind === "line" && !!entry.hashlineAnchorContent);
|
|
116
|
+
const lineNumberWidth = getLineNumberWidth(renderEntries, showHashlineAnchors);
|
|
97
117
|
const palette = resolveDiffPalette(theme);
|
|
98
118
|
// Rich diffs use ccstyle's self shell. Keep the panel transparent so the
|
|
99
119
|
// separator cannot leak toolSuccessBg across the entire new column.
|
|
100
120
|
const containerBgAnsi = undefined;
|
|
101
121
|
const language = resolveLanguageFromPath(options.filePath);
|
|
102
122
|
const cache = createDiffRenderCache();
|
|
103
|
-
const highlightLine = createCodeLineHighlighter(language, theme,
|
|
123
|
+
const highlightLine = createCodeLineHighlighter(language, theme, renderEntries, () => {
|
|
104
124
|
cache.invalidate();
|
|
105
125
|
options.invalidate?.();
|
|
106
126
|
});
|
|
@@ -149,7 +169,7 @@ export function renderEditDiffResult(
|
|
|
149
169
|
const processBudget = resolveDiffProcessBudget(displayLimit, wordWrap);
|
|
150
170
|
// Only highlight/render a prefix that can fill the display limit; full-diff
|
|
151
171
|
// LCS + syntax highlight on thousands of hidden lines is pure waste when collapsed.
|
|
152
|
-
const entryBudget = takeEntriesForLineBudget(
|
|
172
|
+
const entryBudget = takeEntriesForLineBudget(renderEntries, processBudget);
|
|
153
173
|
const splitBudget = takeSplitRowsForBudget(splitRows, processBudget);
|
|
154
174
|
const inlineHighlights = buildInlineHighlightMap(splitBudget.rows);
|
|
155
175
|
const renderCtx: DiffRenderContext = {
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { normalizeCodeWhitespace } from "./diff-text.ts";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
DiffLineEntry,
|
|
4
|
+
DiffMetaEntry,
|
|
5
|
+
DiffOmissionEntry,
|
|
6
|
+
ParsedDiffEntry,
|
|
7
|
+
} from "./diff-parse.ts";
|
|
3
8
|
|
|
4
9
|
export interface DiffSpan {
|
|
5
10
|
start: number;
|
|
@@ -9,6 +14,7 @@ export interface DiffSpan {
|
|
|
9
14
|
export interface SplitDiffRow {
|
|
10
15
|
left?: DiffLineEntry;
|
|
11
16
|
right?: DiffLineEntry;
|
|
17
|
+
omission?: DiffOmissionEntry;
|
|
12
18
|
meta?: DiffMetaEntry;
|
|
13
19
|
hunkIndex: number | null;
|
|
14
20
|
}
|
|
@@ -240,6 +246,12 @@ export function buildSplitRows(entries: ParsedDiffEntry[]): SplitDiffRow[] {
|
|
|
240
246
|
break;
|
|
241
247
|
}
|
|
242
248
|
|
|
249
|
+
if (entry.kind === "omission") {
|
|
250
|
+
rows.push({ omission: entry, hunkIndex: entry.hunkIndex || null });
|
|
251
|
+
index++;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
243
255
|
if (entry.kind !== "line") {
|
|
244
256
|
rows.push({ meta: entry, hunkIndex: entry.hunkIndex || null });
|
|
245
257
|
index++;
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type DiffLineEntry,
|
|
17
17
|
type DiffLineKind,
|
|
18
18
|
type DiffMetaEntry,
|
|
19
|
+
type DiffOmissionEntry,
|
|
19
20
|
type ParsedDiffEntry,
|
|
20
21
|
} from "./diff-parse.ts";
|
|
21
22
|
import { getCellLineNumber, type DiffSpan, type SplitDiffRow } from "./diff-inline.ts";
|
|
@@ -27,6 +28,8 @@ export interface RenderedRow {
|
|
|
27
28
|
hunkIndex: number | null;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
type MaterializedDiffEntry = DiffLineEntry | DiffMetaEntry;
|
|
32
|
+
|
|
30
33
|
interface LineCellRenderParams {
|
|
31
34
|
kind: DiffLineKind;
|
|
32
35
|
code: string;
|
|
@@ -51,6 +54,7 @@ export interface DiffRenderContext {
|
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
const SPLIT_SEPARATOR = " │ ";
|
|
57
|
+
const OMISSION_GLYPH = "⋮";
|
|
54
58
|
const MIN_SPLIT_COLUMN_WIDTH = 24;
|
|
55
59
|
|
|
56
60
|
function resolveIndicatorGlyph(
|
|
@@ -364,6 +368,29 @@ function renderLineCell(
|
|
|
364
368
|
);
|
|
365
369
|
}
|
|
366
370
|
|
|
371
|
+
// Reuse line-cell layout without treating omissions as parsed source lines.
|
|
372
|
+
function createOmissionRenderLine(entry: DiffOmissionEntry, compact = false): DiffLineEntry {
|
|
373
|
+
return {
|
|
374
|
+
kind: "line",
|
|
375
|
+
lineKind: "context",
|
|
376
|
+
oldLineNumber: null,
|
|
377
|
+
newLineNumber: null,
|
|
378
|
+
fallbackLineNumber: compact ? "" : OMISSION_GLYPH,
|
|
379
|
+
content: compact ? OMISSION_GLYPH : "",
|
|
380
|
+
raw: entry.raw,
|
|
381
|
+
hunkIndex: entry.hunkIndex,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function materializeOmissionEntries(
|
|
386
|
+
entries: ParsedDiffEntry[],
|
|
387
|
+
compact = false,
|
|
388
|
+
): MaterializedDiffEntry[] {
|
|
389
|
+
return entries.map((entry) =>
|
|
390
|
+
entry.kind === "omission" ? createOmissionRenderLine(entry, compact) : entry,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
367
394
|
function pushDiffLineRows(rows: RenderedRow[], lines: string[], entry: DiffLineEntry): void {
|
|
368
395
|
rows.push(
|
|
369
396
|
...lines.map((text) => ({
|
|
@@ -374,7 +401,7 @@ function pushDiffLineRows(rows: RenderedRow[], lines: string[], entry: DiffLineE
|
|
|
374
401
|
}
|
|
375
402
|
|
|
376
403
|
function processDiffEntries(
|
|
377
|
-
entries:
|
|
404
|
+
entries: MaterializedDiffEntry[],
|
|
378
405
|
ctx: DiffRenderContext,
|
|
379
406
|
processLine: (entry: DiffLineEntry) => string[],
|
|
380
407
|
): RenderedRow[] {
|
|
@@ -395,7 +422,7 @@ export function renderUnified(
|
|
|
395
422
|
ctx: DiffRenderContext,
|
|
396
423
|
lineNumberWidth: number,
|
|
397
424
|
): RenderedRow[] {
|
|
398
|
-
return processDiffEntries(entries, ctx, (entry) => {
|
|
425
|
+
return processDiffEntries(materializeOmissionEntries(entries), ctx, (entry) => {
|
|
399
426
|
const lineNumber =
|
|
400
427
|
entry.lineKind === "add"
|
|
401
428
|
? formatLineNumberLabel(
|
|
@@ -445,6 +472,10 @@ function toUnifiedFallbackRows(
|
|
|
445
472
|
): RenderedRow[] {
|
|
446
473
|
const flattened: ParsedDiffEntry[] = [];
|
|
447
474
|
for (const row of rows) {
|
|
475
|
+
if (row.omission) {
|
|
476
|
+
flattened.push(row.omission);
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
448
479
|
if (row.meta) {
|
|
449
480
|
flattened.push(row.meta);
|
|
450
481
|
continue;
|
|
@@ -460,7 +491,7 @@ function toUnifiedFallbackRows(
|
|
|
460
491
|
}
|
|
461
492
|
|
|
462
493
|
export function renderCompact(entries: ParsedDiffEntry[], ctx: DiffRenderContext): RenderedRow[] {
|
|
463
|
-
return processDiffEntries(entries, ctx, (entry) => {
|
|
494
|
+
return processDiffEntries(materializeOmissionEntries(entries, true), ctx, (entry) => {
|
|
464
495
|
const codeText = normalizeCodeWhitespace(
|
|
465
496
|
getCompactLineRenderContent(entry, ctx.showHashlineAnchors),
|
|
466
497
|
);
|
|
@@ -679,8 +710,9 @@ export function renderSplit(
|
|
|
679
710
|
continue;
|
|
680
711
|
}
|
|
681
712
|
|
|
713
|
+
const omissionLine = row.omission ? createOmissionRenderLine(row.omission) : undefined;
|
|
682
714
|
const leftCells = renderSplitCell(
|
|
683
|
-
row.left,
|
|
715
|
+
omissionLine ?? row.left,
|
|
684
716
|
"left",
|
|
685
717
|
leftWidth,
|
|
686
718
|
splitLineNumberWidth,
|
|
@@ -694,7 +726,7 @@ export function renderSplit(
|
|
|
694
726
|
showHashlineAnchors,
|
|
695
727
|
);
|
|
696
728
|
const rightCells = renderSplitCell(
|
|
697
|
-
row.right,
|
|
729
|
+
omissionLine ?? row.right,
|
|
698
730
|
"right",
|
|
699
731
|
rightWidth,
|
|
700
732
|
splitLineNumberWidth,
|
|
@@ -2,7 +2,7 @@ import { visibleWidth } from "@earendil-works/pi-tui";
|
|
|
2
2
|
import { fitToWidth } from "./diff-text.ts";
|
|
3
3
|
|
|
4
4
|
export type DiffLineKind = "add" | "remove" | "context";
|
|
5
|
-
export type DiffEntryKind = "line" | "meta" | "hunk" | "file";
|
|
5
|
+
export type DiffEntryKind = "line" | "omission" | "meta" | "hunk" | "file";
|
|
6
6
|
|
|
7
7
|
export interface DiffLineEntry {
|
|
8
8
|
kind: "line";
|
|
@@ -16,13 +16,19 @@ export interface DiffLineEntry {
|
|
|
16
16
|
hunkIndex: number;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export interface DiffOmissionEntry {
|
|
20
|
+
kind: "omission";
|
|
21
|
+
raw: string;
|
|
22
|
+
hunkIndex: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
export interface DiffMetaEntry {
|
|
20
|
-
kind: Exclude<DiffEntryKind, "line">;
|
|
26
|
+
kind: Exclude<DiffEntryKind, "line" | "omission">;
|
|
21
27
|
raw: string;
|
|
22
28
|
hunkIndex: number;
|
|
23
29
|
}
|
|
24
30
|
|
|
25
|
-
export type ParsedDiffEntry = DiffLineEntry | DiffMetaEntry;
|
|
31
|
+
export type ParsedDiffEntry = DiffLineEntry | DiffOmissionEntry | DiffMetaEntry;
|
|
26
32
|
|
|
27
33
|
export interface ParsedDiff {
|
|
28
34
|
entries: ParsedDiffEntry[];
|
|
@@ -40,6 +46,9 @@ export interface DiffStats {
|
|
|
40
46
|
|
|
41
47
|
const CANONICAL_LINE_PATTERN = /^([+\- ])(\s*\d+)\|(.*)$/;
|
|
42
48
|
const HASHLINE_ANCHOR_LINE_PATTERN = /^([+\- ])(\s*\d+)#([A-Za-z0-9]+| {2}):(.*)$/;
|
|
49
|
+
// Pi still emits space-separated numbered rows, unlike OMP's pipe-delimited format.
|
|
50
|
+
const PI_LINE_PATTERN = /^([+\- ])(\s*\d+)\s(.*)$/;
|
|
51
|
+
const PI_OMISSION_LINE_PATTERN = /^ {3,}\.\.\.$/;
|
|
43
52
|
const HUNK_HEADER_PATTERN = /^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@(.*)$/;
|
|
44
53
|
const MIN_LINE_NUMBER_WIDTH = 2;
|
|
45
54
|
|
|
@@ -62,7 +71,10 @@ function toParsedDiffLine(
|
|
|
62
71
|
return { lineKind: "context", lineNumber: normalizedLineNumber, content };
|
|
63
72
|
}
|
|
64
73
|
|
|
65
|
-
function parseCanonicalDiffLine(
|
|
74
|
+
function parseCanonicalDiffLine(
|
|
75
|
+
line: string,
|
|
76
|
+
allowPiFormat: boolean,
|
|
77
|
+
): {
|
|
66
78
|
lineKind: DiffLineKind;
|
|
67
79
|
lineNumber: string;
|
|
68
80
|
content: string;
|
|
@@ -80,7 +92,8 @@ function parseCanonicalDiffLine(line: string): {
|
|
|
80
92
|
};
|
|
81
93
|
}
|
|
82
94
|
|
|
83
|
-
const match =
|
|
95
|
+
const match =
|
|
96
|
+
line.match(CANONICAL_LINE_PATTERN) ?? (allowPiFormat ? line.match(PI_LINE_PATTERN) : null);
|
|
84
97
|
return match ? toParsedDiffLine(match[1] ?? " ", match[2] ?? "", match[3] ?? "") : null;
|
|
85
98
|
}
|
|
86
99
|
|
|
@@ -156,6 +169,14 @@ function pushParsedLineEntry(
|
|
|
156
169
|
});
|
|
157
170
|
}
|
|
158
171
|
|
|
172
|
+
function createOmissionEntry(raw: string, hunkIndex: number): DiffOmissionEntry {
|
|
173
|
+
return {
|
|
174
|
+
kind: "omission",
|
|
175
|
+
raw,
|
|
176
|
+
hunkIndex,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
159
180
|
function createMetaEntry(raw: string, hunkIndex: number): DiffMetaEntry {
|
|
160
181
|
return {
|
|
161
182
|
kind: classifyMetaLine(raw),
|
|
@@ -187,12 +208,14 @@ export function parseDiff(diffText: string): ParsedDiff {
|
|
|
187
208
|
let oldLineCursor: number | null = null;
|
|
188
209
|
let newLineCursor: number | null = null;
|
|
189
210
|
let lineNumberDelta = 0;
|
|
211
|
+
let hasHunkHeader = false;
|
|
190
212
|
|
|
191
213
|
for (const rawLine of diffText.replace(/\r/g, "").split("\n")) {
|
|
192
214
|
stats.lines++;
|
|
193
215
|
|
|
194
216
|
const hunkMatch = rawLine.match(HUNK_HEADER_PATTERN);
|
|
195
217
|
if (hunkMatch) {
|
|
218
|
+
hasHunkHeader = true;
|
|
196
219
|
hunkIndex++;
|
|
197
220
|
stats.hunks = Math.max(stats.hunks, hunkIndex);
|
|
198
221
|
oldLineCursor = toNumber(hunkMatch[1]);
|
|
@@ -217,7 +240,14 @@ export function parseDiff(diffText: string): ParsedDiff {
|
|
|
217
240
|
lineNumberDelta = 0;
|
|
218
241
|
}
|
|
219
242
|
|
|
220
|
-
|
|
243
|
+
// Pi pads omitted context with a blank line number; it is not a source row.
|
|
244
|
+
if (!hasHunkHeader && PI_OMISSION_LINE_PATTERN.test(rawLine)) {
|
|
245
|
+
entries.push(createOmissionEntry(rawLine, hunkIndex));
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Pi's headerless format is ambiguous with numeric source text in unified hunks.
|
|
250
|
+
const canonical = parseCanonicalDiffLine(rawLine, !hasHunkHeader);
|
|
221
251
|
if (canonical) {
|
|
222
252
|
hunkIndex = ensureImplicitHunk(hunkIndex);
|
|
223
253
|
stats.hunks = Math.max(stats.hunks, hunkIndex);
|
|
@@ -10,7 +10,12 @@ import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "../../utils/tool-load
|
|
|
10
10
|
import { isToolTuiFullscreen, showMoreHintText } from "./show-more-hint.ts";
|
|
11
11
|
import { stripAnsi, stripBackgroundAnsi, stripLeadingStatusIcon } from "../../utils/ansi-text.ts";
|
|
12
12
|
import { walkComponentTree } from "../../utils/component-tree.ts";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
fitToolCallSummary,
|
|
15
|
+
humanizeToolLabel,
|
|
16
|
+
toolCallSummary,
|
|
17
|
+
type ToolCallSummary,
|
|
18
|
+
} from "./names.ts";
|
|
14
19
|
import {
|
|
15
20
|
patchRegistry,
|
|
16
21
|
TOOL_GROUPING_GENERATION_KEY as GENERATION_KEY,
|
|
@@ -142,8 +147,11 @@ export function paddedBackgroundRow(
|
|
|
142
147
|
return `${bgAnsi}${stable}\x1b[49m`;
|
|
143
148
|
}
|
|
144
149
|
|
|
145
|
-
function toolSummary(tool: any):
|
|
146
|
-
return toolCallSummary(toolName(tool), tool?.args ?? {}, {
|
|
150
|
+
function toolSummary(tool: any): ToolCallSummary {
|
|
151
|
+
return toolCallSummary(toolName(tool), tool?.args ?? {}, {
|
|
152
|
+
variant: "grouping",
|
|
153
|
+
cwd: tool?.cwd,
|
|
154
|
+
});
|
|
147
155
|
}
|
|
148
156
|
|
|
149
157
|
function toolNameList(tools: any[]): string {
|
|
@@ -331,11 +339,14 @@ export class ToolGroupComponent extends Container {
|
|
|
331
339
|
const continuation = index === total - 1 ? " " : "│ ";
|
|
332
340
|
if (!this._expanded) {
|
|
333
341
|
const summary = toolSummary(tool);
|
|
342
|
+
const prefix = ` ${fg("dim", branch)} ${fg(color, statusIcon(toolStatus))} `;
|
|
343
|
+
const detail = fg("dim", summary.detail);
|
|
344
|
+
const mainWidth = Math.max(0, width - visibleWidth(prefix) - visibleWidth(detail));
|
|
334
345
|
lines.push(
|
|
335
346
|
truncateToWidth(
|
|
336
|
-
|
|
347
|
+
`${prefix}${fg("toolTitle", fitToolCallSummary(summary, mainWidth))}${detail}`,
|
|
337
348
|
width,
|
|
338
|
-
"
|
|
349
|
+
"",
|
|
339
350
|
),
|
|
340
351
|
);
|
|
341
352
|
continue;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { posix, win32 } from "node:path";
|
|
2
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
1
3
|
import { config } from "../../config/config.ts";
|
|
2
4
|
import { oneLine } from "../../utils/format.ts";
|
|
3
5
|
|
|
@@ -31,8 +33,107 @@ export type ToolCallSummaryOptions = {
|
|
|
31
33
|
title?: string;
|
|
32
34
|
/** 文案变体;缺省 "default"。 */
|
|
33
35
|
variant?: ToolCallSummaryVariant;
|
|
36
|
+
/** 工具执行目录;仅用于生成展示路径,不修改调用参数。 */
|
|
37
|
+
cwd?: string;
|
|
34
38
|
};
|
|
35
39
|
|
|
40
|
+
export type ToolCallSummary = {
|
|
41
|
+
main: string;
|
|
42
|
+
detail: string;
|
|
43
|
+
/** 路径摘要保留结构,供最终渲染按实际宽度优先保留文件名。 */
|
|
44
|
+
path?: { prefix: string; value: string };
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function pathApi(value: string) {
|
|
48
|
+
if (win32.isAbsolute(value)) return win32;
|
|
49
|
+
if (posix.isAbsolute(value)) return posix;
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** cwd 内绝对路径转相对路径;cwd 外路径保持不变。 */
|
|
54
|
+
export function displayPath(value: unknown, cwd?: string): string {
|
|
55
|
+
const text = oneLine(value, 4096);
|
|
56
|
+
const base = cwd ? oneLine(cwd, 4096) : "";
|
|
57
|
+
const api = pathApi(text);
|
|
58
|
+
if (!api || !base || !api.isAbsolute(base)) return text;
|
|
59
|
+
const relative = api.relative(base, text);
|
|
60
|
+
if (!relative) return api.basename(text) || ".";
|
|
61
|
+
if (relative === ".." || relative.startsWith(`..${api.sep}`) || api.isAbsolute(relative)) {
|
|
62
|
+
return text;
|
|
63
|
+
}
|
|
64
|
+
return relative;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function headToWidth(text: string, width: number, ellipsis = ""): string {
|
|
68
|
+
if (visibleWidth(text) <= width) return text;
|
|
69
|
+
if (width <= 0) return "";
|
|
70
|
+
const suffix = visibleWidth(ellipsis) <= width ? ellipsis : "";
|
|
71
|
+
const contentWidth = width - visibleWidth(suffix);
|
|
72
|
+
let head = "";
|
|
73
|
+
for (const char of Array.from(text)) {
|
|
74
|
+
if (visibleWidth(head + char) > contentWidth) break;
|
|
75
|
+
head += char;
|
|
76
|
+
}
|
|
77
|
+
return head + suffix;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function tailToWidth(text: string, width: number): string {
|
|
81
|
+
if (width <= 0) return "";
|
|
82
|
+
let tail = "";
|
|
83
|
+
for (const char of Array.from(text).reverse()) {
|
|
84
|
+
if (visibleWidth(char + tail) > width) break;
|
|
85
|
+
tail = char + tail;
|
|
86
|
+
}
|
|
87
|
+
return tail;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 中间截断路径:目录保留开头,末尾优先完整保留文件名。 */
|
|
91
|
+
export function truncatePathToWidth(path: string, width: number): string {
|
|
92
|
+
if (visibleWidth(path) <= width) return path;
|
|
93
|
+
if (width <= 1) return width === 1 ? "…" : "";
|
|
94
|
+
const separatorIndex = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
95
|
+
const filename = separatorIndex >= 0 ? path.slice(separatorIndex + 1) : path;
|
|
96
|
+
const filenameWidth = visibleWidth(filename);
|
|
97
|
+
if (separatorIndex >= 0 && filenameWidth + 1 <= width) {
|
|
98
|
+
const separator = path[separatorIndex]!;
|
|
99
|
+
const suffix = `${separator}${filename}`;
|
|
100
|
+
const prefixWidth = width - visibleWidth(suffix) - 1;
|
|
101
|
+
const prefix = headToWidth(path.slice(0, separatorIndex), Math.max(0, prefixWidth));
|
|
102
|
+
return `${prefix}…${prefix ? suffix : filename}`;
|
|
103
|
+
}
|
|
104
|
+
const leftWidth = Math.max(1, Math.floor((width - 1) * 0.45));
|
|
105
|
+
const rightWidth = Math.max(0, width - leftWidth - 1);
|
|
106
|
+
return `${headToWidth(filename, leftWidth)}…${tailToWidth(filename, rightWidth)}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 路径展示的统一入口:相对化后按配置和当前可用宽度截断。 */
|
|
110
|
+
export function formatDisplayPath(value: unknown, cwd: string | undefined, width: number): string {
|
|
111
|
+
return truncatePathToWidth(displayPath(value, cwd), Math.min(width, config.inputClip));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 按最终终端宽度渲染摘要;路径摘要不会再被整行头部截断。 */
|
|
115
|
+
export function fitToolCallSummary(summary: ToolCallSummary, width: number): string {
|
|
116
|
+
if (!summary.path) return headToWidth(summary.main, width, "…");
|
|
117
|
+
const prefix = summary.path.prefix;
|
|
118
|
+
const pathWidth = Math.max(0, width - visibleWidth(prefix) - 1);
|
|
119
|
+
if (pathWidth <= 0) return headToWidth(prefix, width, "…");
|
|
120
|
+
return `${prefix} ${truncatePathToWidth(summary.path.value, Math.min(pathWidth, config.inputClip))}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function pathSummary(
|
|
124
|
+
prefix: string,
|
|
125
|
+
value: unknown,
|
|
126
|
+
cwd: string | undefined,
|
|
127
|
+
detail = "",
|
|
128
|
+
): ToolCallSummary {
|
|
129
|
+
const path = displayPath(value, cwd);
|
|
130
|
+
return {
|
|
131
|
+
main: `${prefix} ${truncatePathToWidth(path, config.inputClip)}`,
|
|
132
|
+
detail,
|
|
133
|
+
path: { prefix, value: path },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
36
137
|
/**
|
|
37
138
|
* 单工具调用摘要({ main, detail })。
|
|
38
139
|
*
|
|
@@ -43,7 +144,7 @@ export function toolCallSummary(
|
|
|
43
144
|
toolName: string,
|
|
44
145
|
args: any,
|
|
45
146
|
opts: ToolCallSummaryOptions = {},
|
|
46
|
-
):
|
|
147
|
+
): ToolCallSummary {
|
|
47
148
|
const title = opts.title ?? humanizeToolLabel(toolName);
|
|
48
149
|
const variant = opts.variant ?? "default";
|
|
49
150
|
if (!args || typeof args !== "object") return { main: title, detail: "" };
|
|
@@ -106,15 +207,13 @@ export function toolCallSummary(
|
|
|
106
207
|
args.offset !== undefined ? `offset=${args.offset}` : "",
|
|
107
208
|
args.limit !== undefined ? `limit=${args.limit}` : "",
|
|
108
209
|
].filter(Boolean);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
detail: details.length ? ` (${details.join(", ")})` : "",
|
|
113
|
-
};
|
|
210
|
+
const detail = details.length ? ` (${details.join(", ")})` : "";
|
|
211
|
+
if (typeof args.path === "string" && args.path) {
|
|
212
|
+
return pathSummary(variant === "grouping" ? "Read" : title, args.path, opts.cwd, detail);
|
|
114
213
|
}
|
|
115
214
|
return {
|
|
116
|
-
main:
|
|
117
|
-
detail
|
|
215
|
+
main: variant === "grouping" ? "Read ..." : title,
|
|
216
|
+
detail,
|
|
118
217
|
};
|
|
119
218
|
}
|
|
120
219
|
if (variant === "grouping") {
|
|
@@ -135,9 +234,11 @@ export function toolCallSummary(
|
|
|
135
234
|
}
|
|
136
235
|
}
|
|
137
236
|
if (variant === "default") {
|
|
237
|
+
const preferredPath = args.path ?? args.file_path;
|
|
238
|
+
if (typeof preferredPath === "string" && preferredPath) {
|
|
239
|
+
return pathSummary(title, preferredPath, opts.cwd);
|
|
240
|
+
}
|
|
138
241
|
const preferred =
|
|
139
|
-
args.path ??
|
|
140
|
-
args.file_path ??
|
|
141
242
|
args.command ??
|
|
142
243
|
args.query ??
|
|
143
244
|
args.question ??
|
|
@@ -42,6 +42,7 @@ export const TOOL_GROUPING_GENERATION_KEY = Symbol.for("pi.ccstyle.tool-grouping
|
|
|
42
42
|
export const TOOL_MOUSE_OWNER_KEY = Symbol.for("pi.ccstyle.tool-mouse-owner");
|
|
43
43
|
export const TOOL_MOUSE_TUI_SLOT = Symbol.for("pi.ccstyle.tool-mouse-tui");
|
|
44
44
|
export const SCROLL_BUTTON_STATE_SLOT = Symbol.for("pi.ccstyle.scroll-button-state");
|
|
45
|
+
export const OFFICIAL_SCROLL_TO_END_KEY = Symbol.for("pi.ccstyle.official-scroll-to-end");
|
|
45
46
|
export const FLUSH_DOCKED_BASH_PATCH = Symbol.for("pi.ccstyle.flush-docked-bash-patch");
|
|
46
47
|
export const TOOL_HOVER_STATE_KEY = Symbol.for("pi.ccstyle.tool-hover-state");
|
|
47
48
|
|