@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 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.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
+ }
@@ -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
- const raw =
205
- process.stdout.columns ||
206
- (process.stderr as { columns?: number }).columns ||
207
- Number.parseInt(process.env.COLUMNS ?? "", 10) ||
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
- function lnum(n: number | null, w: number, fg = FG_LNUM): string {
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
- return `${fg}${" ".repeat(Math.max(0, w - v.length))}${v}${RST}`;
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 gutter = `${border}${gutterBg}${lnum(num, nw, numFg)}${signFg}${sign}${RST} ${DIVIDER} `;
551
- const contGutter = `${border}${gutterBg}${" ".repeat(nw + 1)}${RST} ${DIVIDER} `;
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
- 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} `;
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 {