pi-pretty-tui 0.3.3 → 0.3.5
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/edit-diff.ts +106 -0
- package/extensions/index.ts +85 -48
- package/package.json +1 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { getLanguageFromPath, highlightCode } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { mixColors, sliceByColumn, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
type Range = [number, number];
|
|
5
|
+
type Row = { marker: string; number?: string; text: string; styled: string; changes: Range[] };
|
|
6
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
7
|
+
const graphemes = (text: string) => Array.from(segmenter.segment(text), part => part.segment);
|
|
8
|
+
|
|
9
|
+
// Bounded grapheme LCS: long/unrelated lines fall back to highlighting their
|
|
10
|
+
// changed middle. Neither path splits emoji, combining marks or surrogate pairs.
|
|
11
|
+
function changedRanges(before: string, after: string): [Range[], Range[]] {
|
|
12
|
+
const a = graphemes(before), b = graphemes(after);
|
|
13
|
+
let start = 0;
|
|
14
|
+
while (start < a.length && start < b.length && a[start] === b[start]) start++;
|
|
15
|
+
let endA = a.length, endB = b.length;
|
|
16
|
+
while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) { endA--; endB--; }
|
|
17
|
+
const removed: Range[] = [], added: Range[] = [];
|
|
18
|
+
const range = (parts: string[], from: number, to: number): Range =>
|
|
19
|
+
[visibleWidth(parts.slice(0, from).join("")), visibleWidth(parts.slice(0, to).join(""))];
|
|
20
|
+
const n = endA - start, m = endB - start;
|
|
21
|
+
if (!n || !m || n * m > 65536) {
|
|
22
|
+
if (n) removed.push(range(a, start, endA));
|
|
23
|
+
if (m) added.push(range(b, start, endB));
|
|
24
|
+
return [removed, added];
|
|
25
|
+
}
|
|
26
|
+
const matrix = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
|
|
27
|
+
for (let i = n - 1; i >= 0; i--) for (let j = m - 1; j >= 0; j--)
|
|
28
|
+
matrix[i][j] = a[start + i] === b[start + j] ? matrix[i + 1][j + 1] + 1 : Math.max(matrix[i + 1][j], matrix[i][j + 1]);
|
|
29
|
+
let i = 0, j = 0;
|
|
30
|
+
while (i < n || j < m) {
|
|
31
|
+
if (i < n && j < m && a[start + i] === b[start + j]) { i++; j++; }
|
|
32
|
+
else if (i < n && (j === m || matrix[i + 1][j] >= matrix[i][j + 1])) {
|
|
33
|
+
removed.push(range(a, start + i, start + i + 1));
|
|
34
|
+
i++;
|
|
35
|
+
} else {
|
|
36
|
+
added.push(range(b, start + j, start + j + 1));
|
|
37
|
+
j++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const merge = (ranges: Range[]) => ranges.reduce<Range[]>((result, next) => {
|
|
41
|
+
const previous = result[result.length - 1];
|
|
42
|
+
if (previous && previous[1] === next[0]) previous[1] = next[1];
|
|
43
|
+
else result.push(next);
|
|
44
|
+
return result;
|
|
45
|
+
}, []);
|
|
46
|
+
return [merge(removed), merge(added)];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function editDiffLayout(theme: any, diff: string, path?: string) {
|
|
50
|
+
const rows: Row[] = diff.split("\n").map(line => {
|
|
51
|
+
const match = /^([ +\-]) *(\d+) (.*)$/.exec(line);
|
|
52
|
+
const marker = match?.[1] ?? (line.startsWith("+") && !line.startsWith("+++") ? "+" : line.startsWith("-") && !line.startsWith("---") ? "-" : " ");
|
|
53
|
+
const text = (match ? match[3] : /^\s+\.\.\.$/.test(line) ? "..." : line).replace(/\t/g, " ");
|
|
54
|
+
return { marker, number: match?.[2], text, styled: text, changes: [] };
|
|
55
|
+
});
|
|
56
|
+
// Highlight old/new snippets separately so removed code cannot corrupt the
|
|
57
|
+
// syntax state of the new side. Keep all highlighting in the host's theme.
|
|
58
|
+
const language = path ? getLanguageFromPath(path) : undefined;
|
|
59
|
+
for (const excluded of ["+", "-"]) {
|
|
60
|
+
const side = rows.filter(row => row.marker !== excluded);
|
|
61
|
+
let styled: string[];
|
|
62
|
+
try { styled = highlightCode(side.map(row => row.text).join("\n"), language); }
|
|
63
|
+
catch { styled = side.map(row => row.text); }
|
|
64
|
+
side.forEach((row, index) => {
|
|
65
|
+
if (row.marker !== " " || excluded === "+") row.styled = styled[index] ?? row.text;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
for (let index = 0; index < rows.length;) {
|
|
69
|
+
if (rows[index].marker !== "-") { index++; continue; }
|
|
70
|
+
const removed: Row[] = [], added: Row[] = [];
|
|
71
|
+
while (rows[index]?.marker === "-") removed.push(rows[index++]);
|
|
72
|
+
while (rows[index]?.marker === "+") added.push(rows[index++]);
|
|
73
|
+
// Do not guess a line pairing when a replacement changes the line count.
|
|
74
|
+
if (removed.length === added.length) removed.forEach((row, i) => {
|
|
75
|
+
[row.changes, added[i].changes] = changedRanges(row.text, added[i].text);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const numberWidth = rows.reduce((width, row) => Math.max(width, row.number?.length ?? 0), 1);
|
|
79
|
+
let index = 0;
|
|
80
|
+
return (_line: string) => {
|
|
81
|
+
const row = rows[index++];
|
|
82
|
+
const color = row.marker === "+" ? "toolDiffAdded" : row.marker === "-" ? "toolDiffRemoved" : "toolDiffContext";
|
|
83
|
+
const background = row.marker === "+" ? "toolSuccessBg" : row.marker === "-" ? "toolErrorBg" : undefined;
|
|
84
|
+
const paint = (text: string) => background && theme.bg ? theme.bg(background, text) : text;
|
|
85
|
+
const changeBackground = background && theme.colors?.[background] && theme.colors?.[color]
|
|
86
|
+
? mixColors(theme.colors[background], theme.colors[color], 0.3) : undefined;
|
|
87
|
+
const width = visibleWidth(row.text);
|
|
88
|
+
let content = "", cursor = 0;
|
|
89
|
+
for (const [start, end] of row.changes) {
|
|
90
|
+
content += sliceByColumn(row.styled, cursor, start - cursor);
|
|
91
|
+
const changed = sliceByColumn(row.styled, start, end - start);
|
|
92
|
+
// A stronger theme-derived background preserves syntax foregrounds and
|
|
93
|
+
// avoids adding font emphasis to the changed graphemes.
|
|
94
|
+
content += changeBackground && theme.style ? theme.style(changed, { bg: changeBackground }) : paint(changed);
|
|
95
|
+
cursor = end;
|
|
96
|
+
}
|
|
97
|
+
content += sliceByColumn(row.styled, cursor, Math.max(0, width - cursor));
|
|
98
|
+
const gutter = row.number ? `${row.number.padStart(numberWidth)} ${row.marker} ` : " ".repeat(numberWidth + 3);
|
|
99
|
+
return {
|
|
100
|
+
prefix: paint(theme.fg(color, gutter)),
|
|
101
|
+
continuation: paint(" ".repeat(numberWidth + 3)),
|
|
102
|
+
content,
|
|
103
|
+
background: background ? paint : undefined,
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
}
|
package/extensions/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type InlineToken,
|
|
9
9
|
} from "./markdown-emphasis.js";
|
|
10
10
|
import { gapRowsAfterBlock, renderSpacedList } from "./markdown-spacing.js";
|
|
11
|
+
import { editDiffLayout } from "./edit-diff.js";
|
|
11
12
|
import { ThoughtPresentation } from "./thought-presentation.js";
|
|
12
13
|
import { isThirdPartyTool, renderToolProjection, THIRD_PARTY_TOOL, toolLabel, type ToolProjection } from "./tool-presentation.js";
|
|
13
14
|
import { NestedTools, NESTED_SNAPSHOT_ENTRY } from "./nested-tools.js";
|
|
@@ -36,12 +37,15 @@ import {
|
|
|
36
37
|
copyToClipboard,
|
|
37
38
|
getAgentDir,
|
|
38
39
|
getMarkdownTheme,
|
|
40
|
+
getLanguageFromPath,
|
|
41
|
+
highlightCode,
|
|
39
42
|
type ExtensionAPI,
|
|
40
43
|
} from "@earendil-works/pi-coding-agent";
|
|
41
44
|
import {
|
|
42
45
|
type Component,
|
|
43
46
|
Markdown,
|
|
44
47
|
stripTerminalSequences,
|
|
48
|
+
sliceByColumn,
|
|
45
49
|
truncateToWidth,
|
|
46
50
|
TuiAltScreen,
|
|
47
51
|
type TuiMouseEvent,
|
|
@@ -196,14 +200,14 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
196
200
|
let latestActivityStatusGeneration = 0;
|
|
197
201
|
const renderLatestActivityStatus = (status: LatestActivityStatus, width: number, theme: any): string => {
|
|
198
202
|
const severity = status.severity ?? "status";
|
|
199
|
-
const icon = severity === "error" ? "✕" : severity === "success" ? "✓" : "
|
|
203
|
+
const icon = severity === "error" ? "✕ " : severity === "success" ? "✓ " : "";
|
|
200
204
|
const color = severity === "error"
|
|
201
205
|
? "error"
|
|
202
206
|
: severity === "success"
|
|
203
207
|
? "success"
|
|
204
208
|
: "dim";
|
|
205
209
|
const detail = status.detail?.trim() ? ` — ${status.detail.trim()}` : "";
|
|
206
|
-
return truncateToWidth(` ${theme.fg(color, `${icon}
|
|
210
|
+
return truncateToWidth(` ${theme.fg(color, `${icon}${status.title}`)}${theme.fg("dim", detail)}`, Math.max(1, width), "…");
|
|
207
211
|
};
|
|
208
212
|
const refreshLatestActivityWidget = () => {
|
|
209
213
|
if (!currentExtensionUi?.setWidget) return;
|
|
@@ -642,7 +646,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
642
646
|
);
|
|
643
647
|
};
|
|
644
648
|
|
|
645
|
-
const
|
|
649
|
+
const renderStandaloneAlert = (member: ActivityMember, width: number, component?: any): string[] => {
|
|
646
650
|
const run = component && renderMode !== "full" ? errorRun(component) : { hidden: false, count: 1, compact: false };
|
|
647
651
|
if (component) errorDisclosureRows.delete(component);
|
|
648
652
|
if (run.hidden) return [];
|
|
@@ -656,7 +660,9 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
656
660
|
const prefix = heading + (content ? theme.fg("dim", " · ") : "");
|
|
657
661
|
const text = prefix + theme.fg("muted", content) + (count > 1 ? theme.fg("dim", component && expandedErrorRuns.has(component) ? " [Collapse]" : ` [×${count}]`) : "");
|
|
658
662
|
// Wrap rather than truncate; all original detail lines remain accessible.
|
|
659
|
-
|
|
663
|
+
// A single-line notification can be entirely its title. Indenting by
|
|
664
|
+
// that title's length wastes most of the viewport on every continuation.
|
|
665
|
+
const continuation = " ".repeat(Math.min(2, Math.max(0, width - 1)));
|
|
660
666
|
const rows: string[] = [];
|
|
661
667
|
for (const line of text.split("\n")) {
|
|
662
668
|
const wrapped = wrapTextWithAnsi(line, Math.max(1, width - visibleWidth(continuation)));
|
|
@@ -686,8 +692,8 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
686
692
|
};
|
|
687
693
|
|
|
688
694
|
const renderPendingActivityUpdate = (member: ActivityMember, width: number, component?: any): string[] => {
|
|
689
|
-
if (member.updateSeverity === "error"
|
|
690
|
-
return
|
|
695
|
+
if (member.updateSeverity === "error" || member.updateSeverity === "warning") {
|
|
696
|
+
return renderStandaloneAlert(member, width, component);
|
|
691
697
|
}
|
|
692
698
|
const theme = defaultActivityTheme();
|
|
693
699
|
const title = truncateToWidth(member.updateTitle ?? "Update", Math.max(1, width - 2), "…");
|
|
@@ -2797,6 +2803,8 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2797
2803
|
prefix: DisplayValue;
|
|
2798
2804
|
continuation?: DisplayValue;
|
|
2799
2805
|
content: DisplayValue;
|
|
2806
|
+
singleLine?: boolean;
|
|
2807
|
+
background?: (text: string) => string;
|
|
2800
2808
|
};
|
|
2801
2809
|
|
|
2802
2810
|
/**
|
|
@@ -2810,14 +2818,35 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2810
2818
|
for (const row of rows) {
|
|
2811
2819
|
const prefix = typeof row.prefix === "function" ? row.prefix() : row.prefix;
|
|
2812
2820
|
const content = typeof row.content === "function" ? row.content() : row.content;
|
|
2821
|
+
if (row.singleLine) {
|
|
2822
|
+
const inline = content.replace(/\r\n|\r|\n/g, " ");
|
|
2823
|
+
rendered.push(truncateToWidth(prefix + inline, Math.max(1, width), "…"));
|
|
2824
|
+
continue;
|
|
2825
|
+
}
|
|
2813
2826
|
const rawContinuation = row.continuation ?? " ".repeat(visibleWidth(prefix));
|
|
2814
2827
|
const continuation = typeof rawContinuation === "function" ? rawContinuation() : rawContinuation;
|
|
2815
2828
|
const prefixWidth = Math.max(visibleWidth(prefix), visibleWidth(continuation));
|
|
2816
2829
|
const contentWidth = Math.max(1, width - prefixWidth);
|
|
2817
2830
|
const wrapped = wrapTextWithAnsi(content || " ", contentWidth);
|
|
2818
2831
|
|
|
2819
|
-
|
|
2820
|
-
|
|
2832
|
+
const paint = (line: string) => row.background
|
|
2833
|
+
? row.background(line + " ".repeat(Math.max(0, contentWidth - visibleWidth(line)))) : line;
|
|
2834
|
+
const corner = renderMode === "clean" ? stripTerminalSequences(prefix).indexOf("└") : -1;
|
|
2835
|
+
if (corner >= 0 && wrapped.length > 1) {
|
|
2836
|
+
// Close the branch on the final physical row, not the first row of
|
|
2837
|
+
// a logical source line that may wrap or contain explicit newlines.
|
|
2838
|
+
const column = visibleWidth(stripTerminalSequences(prefix).slice(0, corner));
|
|
2839
|
+
const guide = (symbol: string) => sliceByColumn(continuation, 0, column) +
|
|
2840
|
+
defaultActivityTheme().fg("dim", symbol) +
|
|
2841
|
+
sliceByColumn(continuation, column + 1, Math.max(0, visibleWidth(continuation) - column - 1));
|
|
2842
|
+
rendered.push(prefix.replace("└", "│") + paint(wrapped[0] ?? ""));
|
|
2843
|
+
for (let index = 1; index < wrapped.length; index++) {
|
|
2844
|
+
rendered.push(guide(index === wrapped.length - 1 ? "└" : "│") + paint(wrapped[index]));
|
|
2845
|
+
}
|
|
2846
|
+
} else {
|
|
2847
|
+
rendered.push(prefix + paint(wrapped[0] ?? ""));
|
|
2848
|
+
for (const line of wrapped.slice(1)) rendered.push(continuation + paint(line));
|
|
2849
|
+
}
|
|
2821
2850
|
}
|
|
2822
2851
|
|
|
2823
2852
|
return rendered;
|
|
@@ -2830,23 +2859,25 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2830
2859
|
context.state.compactToolStatus = status;
|
|
2831
2860
|
};
|
|
2832
2861
|
|
|
2833
|
-
const callRow = (theme: any, name: string, detail: string, state: any): DisplayRow => ({
|
|
2862
|
+
const callRow = (theme: any, name: string, detail: string, state: any, expanded = false): DisplayRow => ({
|
|
2863
|
+
singleLine: renderMode === "clean" && !expanded,
|
|
2834
2864
|
prefix: () => {
|
|
2835
2865
|
const status = (state.compactToolStatus ?? "running") as ToolStatus;
|
|
2836
2866
|
const color = status === "success" ? "success" : status === "error" ? "error" : "dim";
|
|
2837
2867
|
if (renderMode === "compact") return name === "bash" ? theme.fg("toolTitle", "$ ") : "";
|
|
2838
2868
|
return theme.fg(color, "● ");
|
|
2839
2869
|
},
|
|
2840
|
-
continuation: renderMode === "
|
|
2870
|
+
continuation: renderMode === "clean" && expanded ? theme.fg("dim", "│ ") :
|
|
2871
|
+
renderMode === "compact" && name !== "bash" ? "" : " ",
|
|
2841
2872
|
content: renderMode === "compact" ? theme.fg("muted", detail) :
|
|
2842
2873
|
theme.fg("text", theme.bold(name)) +
|
|
2843
2874
|
theme.fg("dim", "(") +
|
|
2844
|
-
theme.fg("muted", detail) +
|
|
2875
|
+
theme.fg(renderMode === "clean" && expanded ? "text" : "muted", detail) +
|
|
2845
2876
|
theme.fg("dim", ")"),
|
|
2846
2877
|
});
|
|
2847
2878
|
|
|
2848
|
-
const call = (theme: any, name: string, detail: string, state: any) =>
|
|
2849
|
-
block([callRow(theme, name, detail, state)]);
|
|
2879
|
+
const call = (theme: any, name: string, detail: string, state: any, expanded = false) =>
|
|
2880
|
+
block([callRow(theme, name, detail, state, expanded)]);
|
|
2850
2881
|
|
|
2851
2882
|
const writeCall = (theme: any, name: string, path: string, content: string, expanded: boolean, state: any) => {
|
|
2852
2883
|
const lines = content.replace(/\t/g, " ").split("\n");
|
|
@@ -2856,7 +2887,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2856
2887
|
const shown = lines.slice(0, expanded ? total : 10);
|
|
2857
2888
|
const remaining = total - shown.length;
|
|
2858
2889
|
const rows: DisplayRow[] = [
|
|
2859
|
-
callRow(theme, name, `${path} · ${total} ${total === 1 ? "line" : "lines"}`, state),
|
|
2890
|
+
callRow(theme, name, `${path} · ${total} ${total === 1 ? "line" : "lines"}`, state, expanded),
|
|
2860
2891
|
];
|
|
2861
2892
|
|
|
2862
2893
|
for (let index = 0; index < shown.length; index++) {
|
|
@@ -2879,28 +2910,6 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2879
2910
|
return block(rows);
|
|
2880
2911
|
};
|
|
2881
2912
|
|
|
2882
|
-
// Pi diffs encode the marker before the padded line number. Move it into
|
|
2883
|
-
// its own column and keep the whole gutter out of content word wrapping.
|
|
2884
|
-
const editDiffLayout = (theme: any, diff: string): ((line: string) => DisplayRow) => {
|
|
2885
|
-
const parse = (line: string) => /^([ +\-]) *(\d+) (.*)$/.exec(line);
|
|
2886
|
-
const numberWidth = diff.split("\n").reduce((width, line) => Math.max(width, parse(line)?.[2].length ?? 0), 1);
|
|
2887
|
-
const gutterWidth = numberWidth + 3;
|
|
2888
|
-
return (line) => {
|
|
2889
|
-
const match = parse(line);
|
|
2890
|
-
const marker = match?.[1] ?? (
|
|
2891
|
-
line.startsWith("+") && !line.startsWith("+++") ? "+" :
|
|
2892
|
-
line.startsWith("-") && !line.startsWith("---") ? "-" : " "
|
|
2893
|
-
);
|
|
2894
|
-
const color = marker === "+" ? "toolDiffAdded" : marker === "-" ? "toolDiffRemoved" : "toolDiffContext";
|
|
2895
|
-
const gutter = match ? `${match[2].padStart(numberWidth)} ${marker} ` : " ".repeat(gutterWidth);
|
|
2896
|
-
return {
|
|
2897
|
-
prefix: theme.fg(color, gutter),
|
|
2898
|
-
continuation: " ".repeat(gutterWidth),
|
|
2899
|
-
content: theme.fg(color, match ? match[3] : /^\s+\.\.\.$/.test(line) ? "..." : line),
|
|
2900
|
-
};
|
|
2901
|
-
};
|
|
2902
|
-
};
|
|
2903
|
-
|
|
2904
2913
|
const result = (
|
|
2905
2914
|
theme: any,
|
|
2906
2915
|
summary: string,
|
|
@@ -2929,6 +2938,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
2929
2938
|
prefix: theme.fg("dim", isLast ? " └ " : " │ ") + (layout?.prefix ?? ""),
|
|
2930
2939
|
continuation: (isLast ? " " : theme.fg("dim", " │ ")) + (layout?.continuation ?? ""),
|
|
2931
2940
|
content: layout?.content ?? outputStyle(shown[index] || " "),
|
|
2941
|
+
background: layout?.background,
|
|
2932
2942
|
});
|
|
2933
2943
|
}
|
|
2934
2944
|
if (omitted > 0) {
|
|
@@ -3577,7 +3587,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3577
3587
|
const dimPhase = responseRunStartedAt !== undefined &&
|
|
3578
3588
|
Math.floor(Math.max(0, Date.now() - responseRunStartedAt) / RUNNING_DOT_INTERVAL_MS) % 2 === 1;
|
|
3579
3589
|
const color = currentActivity === "done"
|
|
3580
|
-
? collapsedDone ? "dim" : "
|
|
3590
|
+
? collapsedDone ? "dim" : "text"
|
|
3581
3591
|
: dimPhase ? "dim" : "accent";
|
|
3582
3592
|
return currentActivity !== "done" && !dimPhase
|
|
3583
3593
|
? runningColor(theme, "● ")
|
|
@@ -3588,7 +3598,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
3588
3598
|
const currentActivity = typeof activity === "function" ? activity() : activity;
|
|
3589
3599
|
const label = currentActivity === "done" ? "Done" : "Running";
|
|
3590
3600
|
const color = label === "Done"
|
|
3591
|
-
? collapsedDone ? "dim" : "
|
|
3601
|
+
? collapsedDone ? "dim" : "text"
|
|
3592
3602
|
: "accent";
|
|
3593
3603
|
const detailColor = label === "Done" && collapsedDone ? "dim" : "text";
|
|
3594
3604
|
const title = label === "Running"
|
|
@@ -4536,7 +4546,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
4536
4546
|
renderCall(args: any, theme: any, context: any) {
|
|
4537
4547
|
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, readLabel, context.toolCallId);
|
|
4538
4548
|
const range = readLineRange(args.offset, args.limit);
|
|
4539
|
-
return call(theme, readLabel, `${args.path}${range}`, context.state);
|
|
4549
|
+
return call(theme, readLabel, `${args.path}${range}`, context.state, context.expanded);
|
|
4540
4550
|
},
|
|
4541
4551
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
4542
4552
|
const output = textContent(toolResult);
|
|
@@ -4572,9 +4582,9 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
4572
4582
|
const detail = omitted > 0
|
|
4573
4583
|
? `${firstLine} … (${omitted} more ${omitted === 1 ? "line" : "lines"})`
|
|
4574
4584
|
: firstLine;
|
|
4575
|
-
return call(theme, bashLabel, detail, context.state);
|
|
4585
|
+
return call(theme, bashLabel, detail, context.state, context.expanded);
|
|
4576
4586
|
}
|
|
4577
|
-
return call(theme, bashLabel, command, context.state);
|
|
4587
|
+
return call(theme, bashLabel, command, context.state, context.expanded);
|
|
4578
4588
|
},
|
|
4579
4589
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
4580
4590
|
const output = textContent(toolResult);
|
|
@@ -4616,7 +4626,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
4616
4626
|
renderCall(args: any, theme: any, context: any) {
|
|
4617
4627
|
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, editLabel, context.toolCallId);
|
|
4618
4628
|
const count = Array.isArray(args.edits) ? ` · ${args.edits.length} changes` : "";
|
|
4619
|
-
return call(theme, editLabel, `${args.path}${count}`, context.state);
|
|
4629
|
+
return call(theme, editLabel, `${args.path}${count}`, context.state, context.expanded);
|
|
4620
4630
|
},
|
|
4621
4631
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
4622
4632
|
const output = textContent(toolResult);
|
|
@@ -4646,7 +4656,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
4646
4656
|
if (line.startsWith("-") && !line.startsWith("---")) return theme.fg("toolDiffRemoved", line);
|
|
4647
4657
|
return theme.fg("toolDiffContext", line);
|
|
4648
4658
|
},
|
|
4649
|
-
!failed && diff ? editDiffLayout(theme, diff) : undefined,
|
|
4659
|
+
!failed && diff ? editDiffLayout(theme, diff, context.args?.path) : undefined,
|
|
4650
4660
|
);
|
|
4651
4661
|
},
|
|
4652
4662
|
} as any);
|
|
@@ -4660,11 +4670,11 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
4660
4670
|
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, writeLabel, context.toolCallId);
|
|
4661
4671
|
const content = typeof args.content === "string" ? args.content : "";
|
|
4662
4672
|
const path = String(args.path ?? "");
|
|
4663
|
-
if (useCompactToolView(context.toolCallId, context.expanded)) {
|
|
4673
|
+
if (renderMode === "clean" || useCompactToolView(context.toolCallId, context.expanded)) {
|
|
4664
4674
|
const lines = content.replace(/\t/g, " ").split("\n");
|
|
4665
4675
|
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
4666
4676
|
const total = lines.length;
|
|
4667
|
-
return call(theme, writeLabel, `${path} · ${total} ${total === 1 ? "line" : "lines"}`, context.state);
|
|
4677
|
+
return call(theme, writeLabel, `${path} · ${total} ${total === 1 ? "line" : "lines"}`, context.state, context.expanded);
|
|
4668
4678
|
}
|
|
4669
4679
|
return writeCall(theme, writeLabel, path, content, context.expanded, context.state);
|
|
4670
4680
|
},
|
|
@@ -4673,6 +4683,33 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
4673
4683
|
if (hideCleanTool(context.toolCallId, options.expanded, context.executionStarted)) return hiddenToolResult(context, options, output);
|
|
4674
4684
|
if (options.isPartial) return partialResult(context, theme, "Writing…");
|
|
4675
4685
|
const failed = completeStatus(context);
|
|
4686
|
+
if (renderMode === "clean") {
|
|
4687
|
+
if (failed) return result(theme, "Write failed", output, options.expanded, true);
|
|
4688
|
+
const status = output ? `Written · ${output}` : "Written";
|
|
4689
|
+
const rows: DisplayRow[] = [{
|
|
4690
|
+
prefix: theme.fg("dim", "└ "), continuation: " ",
|
|
4691
|
+
content: theme.fg("toolOutput", status),
|
|
4692
|
+
}];
|
|
4693
|
+
if (options.expanded) {
|
|
4694
|
+
const content = typeof context.args?.content === "string" ? context.args.content : "";
|
|
4695
|
+
const source = content.replace(/\r\n|\r/g, "\n").replace(/\t/g, " ").split("\n");
|
|
4696
|
+
// A final newline terminates the last line; additional blank lines
|
|
4697
|
+
// are actual file content and retain their own line numbers.
|
|
4698
|
+
if (source.at(-1) === "") source.pop();
|
|
4699
|
+
let highlighted: string[];
|
|
4700
|
+
try { highlighted = highlightCode(source.join("\n"), getLanguageFromPath(String(context.args?.path ?? ""))); }
|
|
4701
|
+
catch { highlighted = source; }
|
|
4702
|
+
const digits = String(source.length).length;
|
|
4703
|
+
for (let index = 0; index < source.length; index++) {
|
|
4704
|
+
rows.push({
|
|
4705
|
+
prefix: theme.fg("dim", index === source.length - 1 ? " └ " : " │ ") + theme.fg("dim", `${String(index + 1).padStart(digits)} `),
|
|
4706
|
+
continuation: (index === source.length - 1 ? " " : theme.fg("dim", " │ ")) + " ".repeat(digits + 1),
|
|
4707
|
+
content: highlighted[index] ?? source[index],
|
|
4708
|
+
});
|
|
4709
|
+
}
|
|
4710
|
+
}
|
|
4711
|
+
return block(rows);
|
|
4712
|
+
}
|
|
4676
4713
|
return result(
|
|
4677
4714
|
theme,
|
|
4678
4715
|
failed ? output.split("\n")[0] || "Write failed" : "Written",
|
|
@@ -4692,7 +4729,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
4692
4729
|
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, grepLabel, context.toolCallId);
|
|
4693
4730
|
const where = args.path ? ` · ${args.path}` : "";
|
|
4694
4731
|
const glob = args.glob ? ` · ${args.glob}` : "";
|
|
4695
|
-
return call(theme, grepLabel, `${args.pattern}${where}${glob}`, context.state);
|
|
4732
|
+
return call(theme, grepLabel, `${args.pattern}${where}${glob}`, context.state, context.expanded);
|
|
4696
4733
|
},
|
|
4697
4734
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
4698
4735
|
const output = textContent(toolResult);
|
|
@@ -4711,7 +4748,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
4711
4748
|
renderShell: "self",
|
|
4712
4749
|
renderCall(args: any, theme: any, context: any) {
|
|
4713
4750
|
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, findLabel, context.toolCallId);
|
|
4714
|
-
return call(theme, findLabel, `${args.pattern}${args.path ? ` · ${args.path}` : ""}`, context.state);
|
|
4751
|
+
return call(theme, findLabel, `${args.pattern}${args.path ? ` · ${args.path}` : ""}`, context.state, context.expanded);
|
|
4715
4752
|
},
|
|
4716
4753
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
4717
4754
|
const output = textContent(toolResult);
|
|
@@ -4730,7 +4767,7 @@ export default function prettyTui(pi: ExtensionAPI) {
|
|
|
4730
4767
|
renderShell: "self",
|
|
4731
4768
|
renderCall(args: any, theme: any, context: any) {
|
|
4732
4769
|
if (hideCleanTool(context.toolCallId, context.expanded, context.executionStarted)) return cleanToolCall(theme, lsLabel, context.toolCallId);
|
|
4733
|
-
return call(theme, lsLabel, args.path ?? ".", context.state);
|
|
4770
|
+
return call(theme, lsLabel, args.path ?? ".", context.state, context.expanded);
|
|
4734
4771
|
},
|
|
4735
4772
|
renderResult(toolResult: any, options: any, theme: any, context: any) {
|
|
4736
4773
|
const output = textContent(toolResult);
|