@xynogen/pix-pretty 1.1.0 → 1.3.0
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/README.md +9 -4
- package/package.json +2 -3
- package/src/ansi.ts +0 -10
- package/src/commands/fff.ts +60 -0
- package/src/diff-render.ts +92 -16
- package/src/image.ts +0 -3
- package/src/index.ts +88 -1452
- package/src/thinking.test.ts +153 -169
- package/src/thinking.ts +115 -68
- package/src/tools/bash.ts +154 -0
- package/src/tools/context.ts +19 -0
- package/src/tools/edit.ts +291 -0
- package/src/tools/find.ts +158 -0
- package/src/tools/grep.ts +202 -0
- package/src/tools/ls.ts +111 -0
- package/src/tools/multi-grep.ts +328 -0
- package/src/tools/read.ts +177 -0
- package/src/tools/write.ts +231 -0
- package/src/tsconfig.json +1 -1
- package/src/types.ts +30 -1
- package/src/utils.ts +45 -2
package/README.md
CHANGED
|
@@ -56,12 +56,17 @@ Both work independently but complement each other for a cohesive visual experien
|
|
|
56
56
|
|
|
57
57
|
## Origin
|
|
58
58
|
|
|
59
|
-
Tool rendering
|
|
59
|
+
Tool rendering replaced `npm:@heyhuynhgiabuu/pi-pretty` (which was previously replaced by `npm:@heyhuynhgiabuu/pi-diff`). This package is a clean reimplementation — no code was copied directly. Developed independently; changes are not submitted back and upstream changes are not pulled in.
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
2. **FFF state dir: `~/.pi/agent/pi-pretty/fff` → `~/.cache/pi/fff`** - Standard XDG cache location
|
|
61
|
+
Key divergences from upstream:
|
|
63
62
|
|
|
64
|
-
|
|
63
|
+
1. **Highlight engine: shiki → cli-highlight** - Simpler, no WASM, synchronous
|
|
64
|
+
2. **FFF state dir** - `~/.pi/agent/pi-pretty/fff` → `~/.cache/pi/fff` (XDG cache)
|
|
65
|
+
3. **Split diff view for edit/write tools** - Full side-by-side diff with gutter, line numbers, syntax highlighting
|
|
66
|
+
4. **Paste chip formatting** - Custom editor component for Pi's paste marker system (not in upstream)
|
|
67
|
+
5. **Reasoning tag rendering** - Collapsible `<think>`/`<thinking>` blocks (not in upstream)
|
|
68
|
+
|
|
69
|
+
Paste chip formatting and reasoning tag rendering are original additions with no upstream equivalent.
|
|
65
70
|
|
|
66
71
|
## License
|
|
67
72
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-pretty",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Enhanced tool output rendering with syntax highlighting, file icons, tree views, FFF search, and paste chip formatting",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -40,8 +40,7 @@
|
|
|
40
40
|
"url": "https://github.com/xynogen/pix-mono/issues"
|
|
41
41
|
},
|
|
42
42
|
"publishConfig": {
|
|
43
|
-
"access": "public"
|
|
44
|
-
"provenance": true
|
|
43
|
+
"access": "public"
|
|
45
44
|
},
|
|
46
45
|
"dependencies": {
|
|
47
46
|
"cli-highlight": "^2.1.11",
|
package/src/ansi.ts
CHANGED
|
@@ -1,29 +1,19 @@
|
|
|
1
1
|
import type { BgTheme } from "./types.js";
|
|
2
2
|
|
|
3
3
|
export let RST = "\x1b[0m";
|
|
4
|
-
|
|
5
4
|
export const BOLD = "\x1b[1m";
|
|
6
5
|
|
|
7
6
|
export const FG_LNUM = "\x1b[38;2;100;100;100m";
|
|
8
|
-
|
|
9
7
|
export const FG_DIM = "\x1b[38;2;80;80;80m";
|
|
10
|
-
|
|
11
8
|
export const FG_RULE = "\x1b[38;2;50;50;50m";
|
|
12
|
-
|
|
13
9
|
export const FG_GREEN = "\x1b[38;2;100;180;120m";
|
|
14
|
-
|
|
15
10
|
export const FG_RED = "\x1b[38;2;200;100;100m";
|
|
16
|
-
|
|
17
11
|
export const FG_YELLOW = "\x1b[38;2;220;180;80m";
|
|
18
|
-
|
|
19
12
|
export const FG_BLUE = "\x1b[38;2;100;140;220m";
|
|
20
|
-
|
|
21
13
|
const FG_MUTED = "\x1b[38;2;139;148;158m";
|
|
22
14
|
|
|
23
15
|
const BG_DEFAULT = "\x1b[49m";
|
|
24
|
-
|
|
25
16
|
export let BG_BASE = BG_DEFAULT; // tool box success/base bg — updated from theme's toolSuccessBg
|
|
26
|
-
|
|
27
17
|
export let BG_ERROR = BG_DEFAULT; // tool box error bg — updated from theme's toolErrorBg
|
|
28
18
|
|
|
29
19
|
/** Parse an ANSI 24-bit color escape into { r, g, b }. Handles both fg (38;2) and bg (48;2). */
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { FffState } from "../fff.js";
|
|
2
|
+
import type { CommandContextLike, PiPrettyApi } from "../types.js";
|
|
3
|
+
|
|
4
|
+
// ── FFF slash commands ─────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export function registerFffCommands(pi: PiPrettyApi, fffState: FffState): void {
|
|
7
|
+
pi.registerCommand("fff-health", {
|
|
8
|
+
description: "Show FFF file finder health and indexer status",
|
|
9
|
+
handler: async (_args: string, ctx: CommandContextLike) => {
|
|
10
|
+
if (!fffState.finder || fffState.finder.isDestroyed) {
|
|
11
|
+
ctx.ui?.notify?.("FFF not initialized", "warning");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const health = fffState.finder.healthCheck();
|
|
16
|
+
if (!health.ok) {
|
|
17
|
+
ctx.ui?.notify?.(`Health check failed: ${health.error}`, "error");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const h = health.value;
|
|
22
|
+
const lines = [
|
|
23
|
+
`FFF v${h.version}`,
|
|
24
|
+
`Git: ${h.git.repositoryFound ? `yes (${h.git.workdir ?? "unknown"})` : "no"}`,
|
|
25
|
+
`Picker: ${h.filePicker.initialized ? `${h.filePicker.indexedFiles ?? 0} files` : "not initialized"}`,
|
|
26
|
+
`Frecency: ${h.frecency.initialized ? "active" : "disabled"}`,
|
|
27
|
+
`Query tracker: ${h.queryTracker.initialized ? "active" : "disabled"}`,
|
|
28
|
+
`Partial index: ${fffState.partialIndex ? "yes (scan timed out)" : "no"}`,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const progress = fffState.finder.getScanProgress();
|
|
32
|
+
if (progress.ok) {
|
|
33
|
+
lines.push(
|
|
34
|
+
`Scanning: ${progress.value.isScanning ? "yes" : "no"} (${progress.value.scannedFilesCount} files)`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
ctx.ui?.notify?.(lines.join("\n"), "info");
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
pi.registerCommand("fff-rescan", {
|
|
43
|
+
description: "Trigger FFF to rescan files",
|
|
44
|
+
handler: async (_args: string, ctx: CommandContextLike) => {
|
|
45
|
+
if (!fffState.finder || fffState.finder.isDestroyed) {
|
|
46
|
+
ctx.ui?.notify?.("FFF not initialized", "warning");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const result = fffState.finder.scanFiles();
|
|
51
|
+
if (!result.ok) {
|
|
52
|
+
ctx.ui?.notify?.(`Rescan failed: ${result.error}`, "error");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fffState.partialIndex = false;
|
|
57
|
+
ctx.ui?.notify?.("FFF rescan triggered", "info");
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
package/src/diff-render.ts
CHANGED
|
@@ -201,12 +201,36 @@ function tabs(s: string): string {
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
function termW(): number {
|
|
204
|
+
// Delegate to utils.termW which has tty ioctl fallback + resize invalidation
|
|
205
|
+
const stderrCols = (process.stderr as { columns?: number }).columns;
|
|
204
206
|
const raw =
|
|
205
207
|
process.stdout.columns ||
|
|
206
|
-
|
|
208
|
+
stderrCols ||
|
|
207
209
|
Number.parseInt(process.env.COLUMNS ?? "", 10) ||
|
|
210
|
+
_readTtyColsDR() ||
|
|
208
211
|
DEFAULT_TERM_WIDTH;
|
|
209
|
-
return Math.max(80, Math.min(raw
|
|
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;
|
|
210
234
|
}
|
|
211
235
|
|
|
212
236
|
/** Pad/truncate `s` to exactly `w` visible chars. ANSI-aware. */
|
|
@@ -335,10 +359,53 @@ function stripes(w: number, _rowOffset: number): string {
|
|
|
335
359
|
return BG_BASE + FG_STRIPE + "╱".repeat(w) + RST;
|
|
336
360
|
}
|
|
337
361
|
|
|
338
|
-
|
|
362
|
+
/** Right-aligned line number. `noReset` keeps the active bg alive (no
|
|
363
|
+
* trailing RST) so the caller can build one bg-continuous gutter segment. */
|
|
364
|
+
function lnum(
|
|
365
|
+
n: number | null,
|
|
366
|
+
w: number,
|
|
367
|
+
fg = FG_LNUM,
|
|
368
|
+
noReset = false,
|
|
369
|
+
): string {
|
|
339
370
|
if (n === null) return " ".repeat(w);
|
|
340
371
|
const v = String(n);
|
|
341
|
-
|
|
372
|
+
const pad = " ".repeat(Math.max(0, w - v.length));
|
|
373
|
+
return noReset ? `${fg}${pad}${v}` : `${fg}${pad}${v}${RST}`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Build one bg-continuous gutter row. A single `gutterBg` is set up front and
|
|
377
|
+
* only foreground colors switch inside it (fg changes never reset bg), so no
|
|
378
|
+
* internal RST can punch a dark gap. The trailing space adopts `bodyBg` to
|
|
379
|
+
* blend the gutter into the code body, then one RST closes the segment. */
|
|
380
|
+
function buildGutter(opts: {
|
|
381
|
+
borderFg: string;
|
|
382
|
+
gutterBg: string;
|
|
383
|
+
bodyBg: string;
|
|
384
|
+
num: number | null;
|
|
385
|
+
numFg: string;
|
|
386
|
+
signFg: string;
|
|
387
|
+
sign: string;
|
|
388
|
+
nw: number;
|
|
389
|
+
continuation: boolean;
|
|
390
|
+
}): string {
|
|
391
|
+
const {
|
|
392
|
+
borderFg,
|
|
393
|
+
gutterBg,
|
|
394
|
+
bodyBg,
|
|
395
|
+
num,
|
|
396
|
+
numFg,
|
|
397
|
+
signFg,
|
|
398
|
+
sign,
|
|
399
|
+
nw,
|
|
400
|
+
continuation,
|
|
401
|
+
} = opts;
|
|
402
|
+
const border = borderFg
|
|
403
|
+
? `${gutterBg}${borderFg}${BORDER_BAR}`
|
|
404
|
+
: `${BG_BASE} `;
|
|
405
|
+
const numCell = continuation
|
|
406
|
+
? " ".repeat(nw + 2)
|
|
407
|
+
: `${lnum(num, nw, numFg, true)}${signFg}${sign} `;
|
|
408
|
+
return `${border}${gutterBg}${numCell}${FG_RULE}│${bodyBg} ${RST}`;
|
|
342
409
|
}
|
|
343
410
|
|
|
344
411
|
function rule(w: number): string {
|
|
@@ -545,10 +612,19 @@ export async function renderUnified(
|
|
|
545
612
|
bodyBg = "",
|
|
546
613
|
): void {
|
|
547
614
|
const borderFg = sign === "-" ? dc.fgDel : sign === "+" ? dc.fgAdd : "";
|
|
548
|
-
const border = borderFg ? `${borderFg}${BORDER_BAR}${RST}` : `${BG_BASE} `;
|
|
549
615
|
const numFg = borderFg || FG_LNUM;
|
|
550
|
-
const
|
|
551
|
-
|
|
616
|
+
const gutterArgs = {
|
|
617
|
+
borderFg,
|
|
618
|
+
gutterBg,
|
|
619
|
+
bodyBg,
|
|
620
|
+
num,
|
|
621
|
+
numFg,
|
|
622
|
+
signFg,
|
|
623
|
+
sign,
|
|
624
|
+
nw,
|
|
625
|
+
};
|
|
626
|
+
const gutter = buildGutter({ ...gutterArgs, continuation: false });
|
|
627
|
+
const contGutter = buildGutter({ ...gutterArgs, continuation: true });
|
|
552
628
|
const rows = wrapAnsi(tabs(body), cw, adaptiveWrapRows(), bodyBg);
|
|
553
629
|
out.push(`${gutter}${rows[0]}${RST}`);
|
|
554
630
|
for (let r = 1; r < rows.length; r++)
|
|
@@ -796,7 +872,6 @@ export async function renderSplit(
|
|
|
796
872
|
: line.newNum;
|
|
797
873
|
|
|
798
874
|
const borderFg = isDel ? dc.fgDel : isAdd ? dc.fgAdd : "";
|
|
799
|
-
const border = borderFg ? `${borderFg}${BORDER_BAR}${RST}` : ` ${BG_BASE}`;
|
|
800
875
|
const numFg = borderFg || FG_LNUM;
|
|
801
876
|
|
|
802
877
|
let body: string;
|
|
@@ -808,18 +883,19 @@ export async function renderSplit(
|
|
|
808
883
|
body = `${BG_BASE}${DIM}${hl}`;
|
|
809
884
|
}
|
|
810
885
|
|
|
811
|
-
|
|
812
|
-
|
|
886
|
+
// Split view's non-bordered context rows lead with a space before bg;
|
|
887
|
+
// buildGutter handles bordered rows, so feed the same border convention.
|
|
888
|
+
const splitBorder = borderFg
|
|
889
|
+
? `${gBg}${borderFg}${BORDER_BAR}`
|
|
890
|
+
: ` ${BG_BASE}`;
|
|
891
|
+
const numCell = `${lnum(num, nw, numFg, true)}${sFg}${BOLD}${sign} `;
|
|
892
|
+
const gutter = `${splitBorder}${gBg}${numCell}${FG_RULE}│${cBg} ${RST}`;
|
|
893
|
+
const contGutter = `${splitBorder}${gBg}${" ".repeat(nw + 2)}${FG_RULE}│${cBg} ${RST}`;
|
|
813
894
|
const bodyRows = wrapAnsi(tabs(body), cw, adaptiveWrapRows(), cBg);
|
|
814
895
|
return { gutter, contGutter, bodyRows };
|
|
815
896
|
}
|
|
816
897
|
|
|
817
898
|
const out: string[] = [];
|
|
818
|
-
const hdrOld = `${BG_BASE}${" ".repeat(Math.max(0, nw - 2))}${dc.fgDel}${DIM}old${RST}`;
|
|
819
|
-
const hdrNew = `${BG_BASE}${" ".repeat(Math.max(0, nw - 2))}${dc.fgAdd}${DIM}new${RST}`;
|
|
820
|
-
out.push(
|
|
821
|
-
`${BG_BASE}${hdrOld}${" ".repeat(Math.max(0, half - nw - 1))}${FG_RULE}┊${RST}${hdrNew}`,
|
|
822
|
-
);
|
|
823
899
|
out.push(`${rule(half)}${FG_RULE}┊${RST}${rule(half)}`);
|
|
824
900
|
|
|
825
901
|
for (const r of vis) {
|
|
@@ -877,7 +953,7 @@ export async function renderSplit(
|
|
|
877
953
|
(rightIsEmpty
|
|
878
954
|
? stripes(cw, stripeRow)
|
|
879
955
|
: `${BG_EMPTY}${" ".repeat(cw)}${RST}`);
|
|
880
|
-
out.push(`${lg}${lb}${DIVIDER}${rg}${rb}`);
|
|
956
|
+
out.push(`${lg}${lb}${DIVIDER}${rg}${rb}${RST}`);
|
|
881
957
|
stripeRow++;
|
|
882
958
|
}
|
|
883
959
|
}
|
package/src/image.ts
CHANGED
|
@@ -3,11 +3,8 @@ import * as childProcess from "node:child_process";
|
|
|
3
3
|
import type { ImageProtocol } from "./types.js";
|
|
4
4
|
|
|
5
5
|
let _tmuxClientTermCache: string | null | undefined;
|
|
6
|
-
|
|
7
6
|
let _tmuxAllowPassthroughCache: boolean | null | undefined;
|
|
8
|
-
|
|
9
7
|
let _tmuxClientTermOverrideForTests: string | null | undefined;
|
|
10
|
-
|
|
11
8
|
let _tmuxAllowPassthroughOverrideForTests: boolean | null | undefined;
|
|
12
9
|
|
|
13
10
|
function isTmuxSession(): boolean {
|