@xynogen/pix-pretty 1.3.0 → 1.3.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/package.json +1 -1
- package/src/diff-render.ts +5 -31
- package/src/tools/bash.test.ts +86 -0
- package/src/tools/bash.ts +28 -9
- package/src/tools/find.ts +1 -0
- package/src/tools/grep.ts +1 -0
- package/src/tools/ls.ts +1 -0
- package/src/tools/multi-grep.ts +1 -0
- package/src/tools/read.ts +3 -0
- package/src/tools/write.ts +1 -0
- package/src/utils.ts +15 -8
package/package.json
CHANGED
package/src/diff-render.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { MAX_HL_CHARS, MAX_RENDER_LINES, WORD_DIFF_MIN_SIM } from "./config.js";
|
|
|
14
14
|
import type { DiffLine, ParsedDiff } from "./diff.js";
|
|
15
15
|
import { hlBlock } from "./highlight.js";
|
|
16
16
|
import type { BundledLanguage } from "./types.js";
|
|
17
|
+
import { termW as utilsTermW } from "./utils.js";
|
|
17
18
|
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
// Env-overridable color/threshold helpers (mirror pi-diff)
|
|
@@ -73,7 +74,6 @@ const ANSI_CAPTURE_RE = new RegExp(`${ESC_RE}\\[([^m]*)m`, "g");
|
|
|
73
74
|
// ---------------------------------------------------------------------------
|
|
74
75
|
|
|
75
76
|
const MAX_TERM_WIDTH = 210;
|
|
76
|
-
const DEFAULT_TERM_WIDTH = 200;
|
|
77
77
|
|
|
78
78
|
const MAX_PREVIEW_LINES = envInt("PRETTY_MAX_PREVIEW_LINES", 80);
|
|
79
79
|
|
|
@@ -201,36 +201,10 @@ function tabs(s: string): string {
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
function termW(): number {
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
stderrCols ||
|
|
209
|
-
Number.parseInt(process.env.COLUMNS ?? "", 10) ||
|
|
210
|
-
_readTtyColsDR() ||
|
|
211
|
-
DEFAULT_TERM_WIDTH;
|
|
212
|
-
return Math.max(80, Math.min(raw, MAX_TERM_WIDTH));
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function _readTtyColsDR(): number | undefined {
|
|
216
|
-
try {
|
|
217
|
-
const { getWindowSize } = require("node:tty") as {
|
|
218
|
-
getWindowSize?: (fd: number) => [number, number];
|
|
219
|
-
};
|
|
220
|
-
if (getWindowSize) {
|
|
221
|
-
for (const fd of [1, 2, 0]) {
|
|
222
|
-
try {
|
|
223
|
-
const [cols] = getWindowSize(fd);
|
|
224
|
-
if (cols && cols > 0) return cols;
|
|
225
|
-
} catch {
|
|
226
|
-
/* not a tty */
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
} catch {
|
|
231
|
-
/* tty unavailable */
|
|
232
|
-
}
|
|
233
|
-
return undefined;
|
|
204
|
+
// Single source of truth: utils.termW caches, falls back to tty ioctl, and
|
|
205
|
+
// invalidates on resize. Diff layout needs a hard floor of 80 cols for the
|
|
206
|
+
// split-view column math, so clamp the shared value here.
|
|
207
|
+
return Math.max(80, Math.min(utilsTermW(), MAX_TERM_WIDTH));
|
|
234
208
|
}
|
|
235
209
|
|
|
236
210
|
/** Pad/truncate `s` to exactly `w` visible chars. ANSI-aware. */
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { registerBashTool } from "./bash";
|
|
4
|
+
|
|
5
|
+
class MockTextComponent {
|
|
6
|
+
private text: string;
|
|
7
|
+
|
|
8
|
+
constructor(text = "") {
|
|
9
|
+
this.text = text;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
setText(value: string): void {
|
|
13
|
+
this.text = value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getText(): string {
|
|
17
|
+
return this.text;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("registerBashTool", () => {
|
|
22
|
+
it("clamps renderCall to small terminal widths", () => {
|
|
23
|
+
const registered: { renderCall?: (...args: any[]) => MockTextComponent } =
|
|
24
|
+
{};
|
|
25
|
+
const origColumns = process.env.COLUMNS;
|
|
26
|
+
process.env.COLUMNS = "24";
|
|
27
|
+
process.stdout.emit("resize");
|
|
28
|
+
process.stdin.emit("resize");
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
registerBashTool(
|
|
32
|
+
{
|
|
33
|
+
registerTool(tool: unknown) {
|
|
34
|
+
Object.assign(registered, tool);
|
|
35
|
+
},
|
|
36
|
+
} as any,
|
|
37
|
+
() => ({
|
|
38
|
+
execute: async () => ({
|
|
39
|
+
content: [{ type: "text", text: "ok" }],
|
|
40
|
+
details: undefined,
|
|
41
|
+
}),
|
|
42
|
+
}),
|
|
43
|
+
{
|
|
44
|
+
cwd: process.cwd(),
|
|
45
|
+
sp: (p: string) => p,
|
|
46
|
+
TextComponent: MockTextComponent as any,
|
|
47
|
+
fffState: {} as any,
|
|
48
|
+
cursorStore: {} as any,
|
|
49
|
+
multiGrepRipgrepFallback: async () => ({
|
|
50
|
+
text: "",
|
|
51
|
+
matchCount: 0,
|
|
52
|
+
limitReached: false,
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const text = registered.renderCall?.(
|
|
58
|
+
{
|
|
59
|
+
command: 'printf "very very very long line"\necho second\necho third',
|
|
60
|
+
timeout: 30,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
fg: (_key: string, value: string) => value,
|
|
64
|
+
bold: (value: string) => value,
|
|
65
|
+
} as any,
|
|
66
|
+
{
|
|
67
|
+
expanded: false,
|
|
68
|
+
isError: false,
|
|
69
|
+
invalidate: () => {},
|
|
70
|
+
state: {},
|
|
71
|
+
} as any,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
expect(text).toBeDefined();
|
|
75
|
+
const rendered = text?.getText() ?? "";
|
|
76
|
+
for (const line of rendered.split("\n")) {
|
|
77
|
+
expect(visibleWidth(line)).toBeLessThanOrEqual(24);
|
|
78
|
+
}
|
|
79
|
+
} finally {
|
|
80
|
+
if (origColumns === undefined) delete process.env.COLUMNS;
|
|
81
|
+
else process.env.COLUMNS = origColumns;
|
|
82
|
+
process.stdout.emit("resize");
|
|
83
|
+
process.stdin.emit("resize");
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
package/src/tools/bash.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
ExtensionContext,
|
|
5
5
|
} from "@earendil-works/pi-coding-agent";
|
|
6
6
|
|
|
7
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
7
8
|
import { FG_DIM, RST } from "../ansi.js";
|
|
8
9
|
import { MAX_PREVIEW_LINES } from "../config.js";
|
|
9
10
|
import { renderBashOutput } from "../renderers.js";
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
fillToolBackground,
|
|
20
21
|
getTextContent,
|
|
21
22
|
isTextContent,
|
|
23
|
+
normalizeLineEndings,
|
|
22
24
|
renderToolError,
|
|
23
25
|
rule,
|
|
24
26
|
setResultDetails,
|
|
@@ -37,6 +39,9 @@ export function registerBashTool(
|
|
|
37
39
|
pi.registerTool({
|
|
38
40
|
...origBash,
|
|
39
41
|
name: "bash",
|
|
42
|
+
// Full-width framing (rules + bg fill) baked at termW(); the default
|
|
43
|
+
// Box shell pads x by 1 and re-wraps at width-2, splitting every line.
|
|
44
|
+
renderShell: "self",
|
|
40
45
|
|
|
41
46
|
async execute(
|
|
42
47
|
tid: string,
|
|
@@ -84,17 +89,28 @@ export function registerBashTool(
|
|
|
84
89
|
renderCtx: RenderContextLike,
|
|
85
90
|
) {
|
|
86
91
|
const cmd = args.command ?? "";
|
|
92
|
+
const displayCmdRaw = cmd.trim();
|
|
87
93
|
const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
|
|
94
|
+
const label = theme.fg("toolTitle", theme.bold("bash"));
|
|
88
95
|
const timeout = args.timeout
|
|
89
96
|
? ` ${theme.fg("muted", `(${args.timeout}s timeout)`)}`
|
|
90
97
|
: "";
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
`${
|
|
96
|
-
|
|
98
|
+
const cmdLines = displayCmdRaw.split("\n");
|
|
99
|
+
const firstLine = cmdLines[0] ?? "";
|
|
100
|
+
const compactCmd =
|
|
101
|
+
cmdLines.length > 1
|
|
102
|
+
? `${firstLine} ${theme.fg("muted", `… (+${cmdLines.length - 1} lines)`)}`
|
|
103
|
+
: firstLine;
|
|
104
|
+
const baseCmd = renderCtx.expanded ? displayCmdRaw : compactCmd;
|
|
105
|
+
const availableWidth = Math.max(1, termW() - 1);
|
|
106
|
+
const prefix = `${label} `;
|
|
107
|
+
const reserve = Math.max(0, availableWidth - timeout.length);
|
|
108
|
+
const displayCmd = truncateToWidth(
|
|
109
|
+
theme.fg("accent", baseCmd),
|
|
110
|
+
Math.max(1, reserve - prefix.length),
|
|
111
|
+
"…",
|
|
97
112
|
);
|
|
113
|
+
text.setText(fillToolBackground(`${prefix}${displayCmd}${timeout}`));
|
|
98
114
|
return text;
|
|
99
115
|
},
|
|
100
116
|
|
|
@@ -113,17 +129,20 @@ export function registerBashTool(
|
|
|
113
129
|
|
|
114
130
|
const d = result.details as Record<string, unknown> | undefined;
|
|
115
131
|
if (d?._type === "bashResult") {
|
|
132
|
+
const normalizedText = normalizeLineEndings(d.text as string)
|
|
133
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
134
|
+
.replace(/^\n+|\n+$/g, "");
|
|
116
135
|
const { summary } = renderBashOutput(
|
|
117
|
-
|
|
136
|
+
normalizedText,
|
|
118
137
|
d.exitCode as number | null,
|
|
119
138
|
);
|
|
120
|
-
const lines =
|
|
139
|
+
const lines = normalizedText ? normalizedText.split("\n") : [];
|
|
121
140
|
const lineCount = lines.length;
|
|
122
141
|
const lineInfo =
|
|
123
142
|
lineCount > 1 ? ` ${FG_DIM}(${lineCount} lines)${RST}` : "";
|
|
124
143
|
const header = ` ${summary}${lineInfo}`;
|
|
125
144
|
|
|
126
|
-
if (
|
|
145
|
+
if (normalizedText) {
|
|
127
146
|
const maxShow = renderCtx.expanded ? lineCount : MAX_PREVIEW_LINES;
|
|
128
147
|
const show = lines.slice(0, maxShow);
|
|
129
148
|
const tw = termW();
|
package/src/tools/find.ts
CHANGED
package/src/tools/grep.ts
CHANGED
package/src/tools/ls.ts
CHANGED
package/src/tools/multi-grep.ts
CHANGED
|
@@ -51,6 +51,7 @@ export function registerMultiGrepTool(
|
|
|
51
51
|
pi.registerTool({
|
|
52
52
|
name: "multi_grep",
|
|
53
53
|
label: "multi_grep",
|
|
54
|
+
renderShell: "self",
|
|
54
55
|
description: [
|
|
55
56
|
"Search file contents for lines matching ANY of multiple patterns (OR logic).",
|
|
56
57
|
"Uses SIMD-accelerated Aho-Corasick multi-pattern matching when FFF is available.",
|
package/src/tools/read.ts
CHANGED
|
@@ -39,6 +39,9 @@ export function registerReadTool(
|
|
|
39
39
|
pi.registerTool({
|
|
40
40
|
...origRead,
|
|
41
41
|
name: "read",
|
|
42
|
+
// Full-width framing baked at termW(); default Box shell pads x by 1
|
|
43
|
+
// and re-wraps at width-2, splitting every line into a padding row.
|
|
44
|
+
renderShell: "self",
|
|
42
45
|
|
|
43
46
|
async execute(
|
|
44
47
|
tid: string,
|
package/src/tools/write.ts
CHANGED
package/src/utils.ts
CHANGED
|
@@ -49,11 +49,26 @@ export function fillToolBackground(text: string, bg = BG_BASE): string {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
let _cachedTermW: number | undefined;
|
|
52
|
+
let _termWResizeBound = false;
|
|
53
|
+
|
|
54
|
+
function _bindTermWResize(): void {
|
|
55
|
+
if (_termWResizeBound) return;
|
|
56
|
+
_termWResizeBound = true;
|
|
57
|
+
// Persistent listeners: every SIGWINCH invalidates the cache so the next
|
|
58
|
+
// termW() re-reads. `.once` only caught the first resize, leaving width
|
|
59
|
+
// stale on subsequent resizes.
|
|
60
|
+
const invalidate = () => {
|
|
61
|
+
_cachedTermW = undefined;
|
|
62
|
+
};
|
|
63
|
+
process.stdout.on("resize", invalidate);
|
|
64
|
+
process.stdin.on("resize", invalidate);
|
|
65
|
+
}
|
|
52
66
|
|
|
53
67
|
/** Read terminal width — checks all available sources in priority order.
|
|
54
68
|
* Falls back to querying the controlling tty via fd 1/2/stdin ioctl.
|
|
55
69
|
* Result is cached and invalidated on SIGWINCH / stdout resize. */
|
|
56
70
|
export function termW(): number {
|
|
71
|
+
_bindTermWResize();
|
|
57
72
|
if (_cachedTermW !== undefined) return _cachedTermW;
|
|
58
73
|
|
|
59
74
|
const stderrWithColumns = process.stderr as NodeJS.WriteStream & {
|
|
@@ -67,14 +82,6 @@ export function termW(): number {
|
|
|
67
82
|
120;
|
|
68
83
|
_cachedTermW = Math.max(1, Math.min(raw, 210));
|
|
69
84
|
|
|
70
|
-
// Invalidate on resize so next call re-reads
|
|
71
|
-
process.stdout.once("resize", () => {
|
|
72
|
-
_cachedTermW = undefined;
|
|
73
|
-
});
|
|
74
|
-
process.stdin.once("resize", () => {
|
|
75
|
-
_cachedTermW = undefined;
|
|
76
|
-
});
|
|
77
|
-
|
|
78
85
|
return _cachedTermW;
|
|
79
86
|
}
|
|
80
87
|
|