@xynogen/pix-pretty 1.2.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/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 +71 -21
- package/src/image.ts +0 -3
- package/src/index.ts +88 -1452
- package/src/tools/bash.test.ts +86 -0
- package/src/tools/bash.ts +173 -0
- package/src/tools/context.ts +19 -0
- package/src/tools/edit.ts +291 -0
- package/src/tools/find.ts +159 -0
- package/src/tools/grep.ts +203 -0
- package/src/tools/ls.ts +112 -0
- package/src/tools/multi-grep.ts +329 -0
- package/src/tools/read.ts +180 -0
- package/src/tools/write.ts +232 -0
- package/src/tsconfig.json +1 -1
- package/src/types.ts +30 -1
- package/src/utils.ts +52 -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.1",
|
|
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
|
@@ -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,12 +201,10 @@ function tabs(s: string): string {
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
function termW(): number {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
DEFAULT_TERM_WIDTH;
|
|
209
|
-
return Math.max(80, Math.min(raw - 4, MAX_TERM_WIDTH));
|
|
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));
|
|
210
208
|
}
|
|
211
209
|
|
|
212
210
|
/** Pad/truncate `s` to exactly `w` visible chars. ANSI-aware. */
|
|
@@ -335,10 +333,53 @@ function stripes(w: number, _rowOffset: number): string {
|
|
|
335
333
|
return BG_BASE + FG_STRIPE + "╱".repeat(w) + RST;
|
|
336
334
|
}
|
|
337
335
|
|
|
338
|
-
|
|
336
|
+
/** Right-aligned line number. `noReset` keeps the active bg alive (no
|
|
337
|
+
* trailing RST) so the caller can build one bg-continuous gutter segment. */
|
|
338
|
+
function lnum(
|
|
339
|
+
n: number | null,
|
|
340
|
+
w: number,
|
|
341
|
+
fg = FG_LNUM,
|
|
342
|
+
noReset = false,
|
|
343
|
+
): string {
|
|
339
344
|
if (n === null) return " ".repeat(w);
|
|
340
345
|
const v = String(n);
|
|
341
|
-
|
|
346
|
+
const pad = " ".repeat(Math.max(0, w - v.length));
|
|
347
|
+
return noReset ? `${fg}${pad}${v}` : `${fg}${pad}${v}${RST}`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Build one bg-continuous gutter row. A single `gutterBg` is set up front and
|
|
351
|
+
* only foreground colors switch inside it (fg changes never reset bg), so no
|
|
352
|
+
* internal RST can punch a dark gap. The trailing space adopts `bodyBg` to
|
|
353
|
+
* blend the gutter into the code body, then one RST closes the segment. */
|
|
354
|
+
function buildGutter(opts: {
|
|
355
|
+
borderFg: string;
|
|
356
|
+
gutterBg: string;
|
|
357
|
+
bodyBg: string;
|
|
358
|
+
num: number | null;
|
|
359
|
+
numFg: string;
|
|
360
|
+
signFg: string;
|
|
361
|
+
sign: string;
|
|
362
|
+
nw: number;
|
|
363
|
+
continuation: boolean;
|
|
364
|
+
}): string {
|
|
365
|
+
const {
|
|
366
|
+
borderFg,
|
|
367
|
+
gutterBg,
|
|
368
|
+
bodyBg,
|
|
369
|
+
num,
|
|
370
|
+
numFg,
|
|
371
|
+
signFg,
|
|
372
|
+
sign,
|
|
373
|
+
nw,
|
|
374
|
+
continuation,
|
|
375
|
+
} = opts;
|
|
376
|
+
const border = borderFg
|
|
377
|
+
? `${gutterBg}${borderFg}${BORDER_BAR}`
|
|
378
|
+
: `${BG_BASE} `;
|
|
379
|
+
const numCell = continuation
|
|
380
|
+
? " ".repeat(nw + 2)
|
|
381
|
+
: `${lnum(num, nw, numFg, true)}${signFg}${sign} `;
|
|
382
|
+
return `${border}${gutterBg}${numCell}${FG_RULE}│${bodyBg} ${RST}`;
|
|
342
383
|
}
|
|
343
384
|
|
|
344
385
|
function rule(w: number): string {
|
|
@@ -545,10 +586,19 @@ export async function renderUnified(
|
|
|
545
586
|
bodyBg = "",
|
|
546
587
|
): void {
|
|
547
588
|
const borderFg = sign === "-" ? dc.fgDel : sign === "+" ? dc.fgAdd : "";
|
|
548
|
-
const border = borderFg ? `${borderFg}${BORDER_BAR}${RST}` : `${BG_BASE} `;
|
|
549
589
|
const numFg = borderFg || FG_LNUM;
|
|
550
|
-
const
|
|
551
|
-
|
|
590
|
+
const gutterArgs = {
|
|
591
|
+
borderFg,
|
|
592
|
+
gutterBg,
|
|
593
|
+
bodyBg,
|
|
594
|
+
num,
|
|
595
|
+
numFg,
|
|
596
|
+
signFg,
|
|
597
|
+
sign,
|
|
598
|
+
nw,
|
|
599
|
+
};
|
|
600
|
+
const gutter = buildGutter({ ...gutterArgs, continuation: false });
|
|
601
|
+
const contGutter = buildGutter({ ...gutterArgs, continuation: true });
|
|
552
602
|
const rows = wrapAnsi(tabs(body), cw, adaptiveWrapRows(), bodyBg);
|
|
553
603
|
out.push(`${gutter}${rows[0]}${RST}`);
|
|
554
604
|
for (let r = 1; r < rows.length; r++)
|
|
@@ -796,7 +846,6 @@ export async function renderSplit(
|
|
|
796
846
|
: line.newNum;
|
|
797
847
|
|
|
798
848
|
const borderFg = isDel ? dc.fgDel : isAdd ? dc.fgAdd : "";
|
|
799
|
-
const border = borderFg ? `${borderFg}${BORDER_BAR}${RST}` : ` ${BG_BASE}`;
|
|
800
849
|
const numFg = borderFg || FG_LNUM;
|
|
801
850
|
|
|
802
851
|
let body: string;
|
|
@@ -808,18 +857,19 @@ export async function renderSplit(
|
|
|
808
857
|
body = `${BG_BASE}${DIM}${hl}`;
|
|
809
858
|
}
|
|
810
859
|
|
|
811
|
-
|
|
812
|
-
|
|
860
|
+
// Split view's non-bordered context rows lead with a space before bg;
|
|
861
|
+
// buildGutter handles bordered rows, so feed the same border convention.
|
|
862
|
+
const splitBorder = borderFg
|
|
863
|
+
? `${gBg}${borderFg}${BORDER_BAR}`
|
|
864
|
+
: ` ${BG_BASE}`;
|
|
865
|
+
const numCell = `${lnum(num, nw, numFg, true)}${sFg}${BOLD}${sign} `;
|
|
866
|
+
const gutter = `${splitBorder}${gBg}${numCell}${FG_RULE}│${cBg} ${RST}`;
|
|
867
|
+
const contGutter = `${splitBorder}${gBg}${" ".repeat(nw + 2)}${FG_RULE}│${cBg} ${RST}`;
|
|
813
868
|
const bodyRows = wrapAnsi(tabs(body), cw, adaptiveWrapRows(), cBg);
|
|
814
869
|
return { gutter, contGutter, bodyRows };
|
|
815
870
|
}
|
|
816
871
|
|
|
817
872
|
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
873
|
out.push(`${rule(half)}${FG_RULE}┊${RST}${rule(half)}`);
|
|
824
874
|
|
|
825
875
|
for (const r of vis) {
|
|
@@ -877,7 +927,7 @@ export async function renderSplit(
|
|
|
877
927
|
(rightIsEmpty
|
|
878
928
|
? stripes(cw, stripeRow)
|
|
879
929
|
: `${BG_EMPTY}${" ".repeat(cw)}${RST}`);
|
|
880
|
-
out.push(`${lg}${lb}${DIVIDER}${rg}${rb}`);
|
|
930
|
+
out.push(`${lg}${lb}${DIVIDER}${rg}${rb}${RST}`);
|
|
881
931
|
stripeRow++;
|
|
882
932
|
}
|
|
883
933
|
}
|
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 {
|