@xynogen/pix-pretty 1.2.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 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 is a vendored fork of [`@heyhuynhgiabuu/pi-pretty`](https://github.com/buddingnewinsights/pi-pretty) with two key changes:
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
- 1. **Highlight engine: shiki → cli-highlight** - Simpler, synchronous, no WASM
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
- Paste chip formatting is custom logic for Pi's paste marker system.
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.2.0",
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
+ }
@@ -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
- (process.stderr as { columns?: number }).columns ||
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 - 4, MAX_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;
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
- function lnum(n: number | null, w: number, fg = FG_LNUM): string {
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
- return `${fg}${" ".repeat(Math.max(0, w - v.length))}${v}${RST}`;
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 gutter = `${border}${gutterBg}${lnum(num, nw, numFg)}${signFg}${sign}${RST} ${DIVIDER} `;
551
- const contGutter = `${border}${gutterBg}${" ".repeat(nw + 1)}${RST} ${DIVIDER} `;
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
- const gutter = `${border}${gBg}${lnum(num, nw, numFg)}${sFg}${BOLD}${sign}${RST} ${FG_RULE}│${RST} `;
812
- const contGutter = `${border}${gBg}${" ".repeat(nw + 1)}${RST} ${FG_RULE}│${RST} `;
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 {