@xynogen/pix-pretty 1.7.10 → 1.7.12

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-pretty",
3
- "version": "1.7.10",
3
+ "version": "1.7.12",
4
4
  "description": "Enhanced tool output rendering with syntax highlighting, file icons, tree views, diff rendering, and FFF search",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/ansi.ts CHANGED
@@ -23,7 +23,7 @@ function parseAnsiRgb(
23
23
  const m = ansi.match(
24
24
  new RegExp(`${ESC_RE}\\[(?:38|48);2;(\\d+);(\\d+);(\\d+)m`),
25
25
  );
26
- return m ? { r: +m[1], g: +m[2], b: +m[3] } : null;
26
+ return m ? { r: +(m[1] ?? 0), g: +(m[2] ?? 0), b: +(m[3] ?? 0) } : null;
27
27
  }
28
28
 
29
29
  function getThemeBgAnsi(theme: BgTheme, key: string): string | null {
@@ -63,7 +63,7 @@ function isLowContrastShikiFg(params: string): boolean {
63
63
  const parts = params.split(";").map(Number);
64
64
  if (parts.length !== 5 || parts.some((n) => !Number.isFinite(n)))
65
65
  return false;
66
- const [, , r, g, b] = parts;
66
+ const [, , r = 0, g = 0, b = 0] = parts;
67
67
  const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
68
68
  return luminance < 72;
69
69
  }
@@ -62,7 +62,7 @@ export function registerPrettyCommand(pi: PiPrettyApi): void {
62
62
  // Headless / no custom-UI host: cycle to the next mode + notify.
63
63
  if (typeof ui.custom !== "function") {
64
64
  const cur = ICON_MODES.indexOf(getIconMode());
65
- const next = ICON_MODES[(cur + 1) % ICON_MODES.length] as IconMode;
65
+ const next = ICON_MODES[(cur + 1) % ICON_MODES.length] ?? "nerd";
66
66
  applyMode(next);
67
67
  ui.notify(`pix-pretty icons: ${next}`, "info");
68
68
  return;
@@ -82,7 +82,7 @@ export function registerPrettyCommand(pi: PiPrettyApi): void {
82
82
 
83
83
  const choose = (i: number) => {
84
84
  selected = (i + ICON_MODES.length) % ICON_MODES.length;
85
- applyMode(ICON_MODES[selected] as IconMode);
85
+ applyMode(ICON_MODES[selected] ?? "nerd");
86
86
  };
87
87
 
88
88
  return {
@@ -464,9 +464,12 @@ function injectBg(
464
464
  continue;
465
465
  }
466
466
  }
467
- while (ri < ranges.length && vis >= ranges[ri][1]) ri++;
467
+ while (ri < ranges.length && vis >= (ranges[ri] as [number, number])[1])
468
+ ri++;
468
469
  const want =
469
- ri < ranges.length && vis >= ranges[ri][0] && vis < ranges[ri][1];
470
+ ri < ranges.length &&
471
+ vis >= (ranges[ri] as [number, number])[0] &&
472
+ vis < (ranges[ri] as [number, number])[1];
470
473
  if (want !== inHL) {
471
474
  inHL = want;
472
475
  out += inHL ? hlBg : baseBg;
@@ -497,6 +500,12 @@ function plainWordDiff(
497
500
  return { old: o, new: n };
498
501
  }
499
502
 
503
+ /** Type-safe index into an array that noUncheckedIndexedAccess marks as T|undefined.
504
+ * Only call when the index is provably in-bounds (loop condition, length check, etc.). */
505
+ function at<T>(arr: T[], i: number): T {
506
+ return arr[i] as T;
507
+ }
508
+
500
509
  // ---------------------------------------------------------------------------
501
510
  // Split-vs-unified decision
502
511
  // ---------------------------------------------------------------------------
@@ -605,7 +614,7 @@ export async function renderUnified(
605
614
  }
606
615
 
607
616
  while (idx < vis.length) {
608
- const l = vis[idx];
617
+ const l = at(vis, idx);
609
618
 
610
619
  if (l.type === "sep") {
611
620
  const gap = l.newNum;
@@ -638,29 +647,35 @@ export async function renderUnified(
638
647
  }
639
648
 
640
649
  const dels: Array<{ l: DiffLine; hl: string }> = [];
641
- while (idx < vis.length && vis[idx].type === "del") {
642
- dels.push({ l: vis[idx], hl: oldHL[oI] ?? vis[idx].content });
650
+ while (idx < vis.length) {
651
+ const entry = at(vis, idx);
652
+ if (entry.type !== "del") break;
653
+ dels.push({ l: entry, hl: oldHL[oI] ?? entry.content });
643
654
  oI++;
644
655
  idx++;
645
656
  }
646
657
  const adds: Array<{ l: DiffLine; hl: string }> = [];
647
- while (idx < vis.length && vis[idx].type === "add") {
648
- adds.push({ l: vis[idx], hl: newHL[nI] ?? vis[idx].content });
658
+ while (idx < vis.length) {
659
+ const entry = at(vis, idx);
660
+ if (entry.type !== "add") break;
661
+ adds.push({ l: entry, hl: newHL[nI] ?? entry.content });
649
662
  nI++;
650
663
  idx++;
651
664
  }
652
665
 
653
666
  const isPaired = dels.length === 1 && adds.length === 1;
654
667
  const wd = isPaired
655
- ? wordDiffAnalysis(dels[0].l.content, adds[0].l.content)
668
+ ? wordDiffAnalysis(at(dels, 0).l.content, at(adds, 0).l.content)
656
669
  : null;
657
670
  const wdBalanced = wd && wd.oldRanges.length > 0 && wd.newRanges.length > 0;
658
671
 
659
672
  if (isPaired && wdBalanced && wd.similarity >= WORD_DIFF_MIN_SIM && canHL) {
660
- const delBody = injectBg(dels[0].hl, wd.oldRanges, BG_DEL, BG_DEL_W);
661
- const addBody = injectBg(adds[0].hl, wd.newRanges, BG_ADD, BG_ADD_W);
673
+ const del0 = at(dels, 0);
674
+ const add0 = at(adds, 0);
675
+ const delBody = injectBg(del0.hl, wd.oldRanges, BG_DEL, BG_DEL_W);
676
+ const addBody = injectBg(add0.hl, wd.newRanges, BG_ADD, BG_ADD_W);
662
677
  emitRow(
663
- dels[0].l.oldNum,
678
+ del0.l.oldNum,
664
679
  "-",
665
680
  BG_GUTTER_DEL,
666
681
  `${dc.fgDel}${BOLD}`,
@@ -668,7 +683,7 @@ export async function renderUnified(
668
683
  BG_DEL,
669
684
  );
670
685
  emitRow(
671
- adds[0].l.newNum,
686
+ add0.l.newNum,
672
687
  "+",
673
688
  BG_GUTTER_ADD,
674
689
  `${dc.fgAdd}${BOLD}`,
@@ -683,9 +698,11 @@ export async function renderUnified(
683
698
  wd.similarity >= WORD_DIFF_MIN_SIM &&
684
699
  !canHL
685
700
  ) {
686
- const pwd = plainWordDiff(dels[0].l.content, adds[0].l.content);
701
+ const del0 = at(dels, 0);
702
+ const add0 = at(adds, 0);
703
+ const pwd = plainWordDiff(del0.l.content, add0.l.content);
687
704
  emitRow(
688
- dels[0].l.oldNum,
705
+ del0.l.oldNum,
689
706
  "-",
690
707
  BG_GUTTER_DEL,
691
708
  `${dc.fgDel}${BOLD}`,
@@ -693,7 +710,7 @@ export async function renderUnified(
693
710
  BG_DEL,
694
711
  );
695
712
  emitRow(
696
- adds[0].l.newNum,
713
+ add0.l.newNum,
697
714
  "+",
698
715
  BG_GUTTER_ADD,
699
716
  `${dc.fgAdd}${BOLD}`,
@@ -755,7 +772,7 @@ export async function renderSplit(
755
772
  const rows: Row[] = [];
756
773
  let i = 0;
757
774
  while (i < diff.lines.length) {
758
- const l = diff.lines[i];
775
+ const l = at(diff.lines, i);
759
776
  if (l.type === "sep" || l.type === "ctx") {
760
777
  rows.push({ left: l, right: l });
761
778
  i++;
@@ -763,12 +780,16 @@ export async function renderSplit(
763
780
  }
764
781
  const dels: DiffLine[] = [];
765
782
  const adds: DiffLine[] = [];
766
- while (i < diff.lines.length && diff.lines[i].type === "del") {
767
- dels.push(diff.lines[i]);
783
+ while (i < diff.lines.length) {
784
+ const entry = at(diff.lines, i);
785
+ if (entry.type !== "del") break;
786
+ dels.push(entry);
768
787
  i++;
769
788
  }
770
- while (i < diff.lines.length && diff.lines[i].type === "add") {
771
- adds.push(diff.lines[i]);
789
+ while (i < diff.lines.length) {
790
+ const entry = at(diff.lines, i);
791
+ if (entry.type !== "add") break;
792
+ adds.push(entry);
772
793
  i++;
773
794
  }
774
795
  const n = Math.max(dels.length, adds.length);
package/src/diff.ts CHANGED
@@ -4,6 +4,12 @@
4
4
 
5
5
  import * as Diff from "diff";
6
6
 
7
+ /** Type-safe index into an array that noUncheckedIndexedAccess marks as T|undefined.
8
+ * Only call when the index is provably in-bounds (loop condition, length check, etc.). */
9
+ function at<T>(arr: T[], i: number): T {
10
+ return arr[i] as T;
11
+ }
12
+
7
13
  export interface DiffLine {
8
14
  type: "add" | "del" | "ctx" | "sep";
9
15
  oldNum: number | null;
@@ -35,8 +41,9 @@ export function parseDiff(
35
41
 
36
42
  for (let hi = 0; hi < patch.hunks.length; hi++) {
37
43
  if (hi > 0) {
38
- const prev = patch.hunks[hi - 1];
39
- const gap = patch.hunks[hi].oldStart - (prev.oldStart + prev.oldLines);
44
+ const prev = at(patch.hunks, hi - 1);
45
+ const gap =
46
+ at(patch.hunks, hi).oldStart - (prev.oldStart + prev.oldLines);
40
47
  lines.push({
41
48
  type: "sep",
42
49
  oldNum: null,
@@ -44,7 +51,7 @@ export function parseDiff(
44
51
  content: "",
45
52
  });
46
53
  }
47
- const h = patch.hunks[hi];
54
+ const h = at(patch.hunks, hi);
48
55
  const shift = baseLine > 0 ? baseLine - 1 : 0;
49
56
  let oL = h.oldStart + shift;
50
57
  let nL = h.newStart + shift;
@@ -74,6 +74,9 @@ const CATALOG = {
74
74
  "opt.toon": { nerd: "\u{F05C0}", unicode: `\u2662${VS}`, ascii: "Tn" },
75
75
  "opt.ponytail": { nerd: "\u{F0190}", unicode: `\u2667${VS}`, ascii: "Pt" },
76
76
  "opt.title": { nerd: "\u{F0DAB}", unicode: `\u25C8${VS}`, ascii: "*" },
77
+
78
+ // ── subagent widget (pix-subagent) ────────────────────────────────────
79
+ agent: { nerd: "\u{F0BA0}", unicode: `\u2699${VS}`, ascii: "@" },
77
80
  } as const;
78
81
 
79
82
  /** Every valid semantic icon key. */
@@ -16,6 +16,10 @@ import { dirname, join } from "node:path";
16
16
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
17
17
  import { ICON_MODES, type IconMode, setIconMode } from "./icon-catalog.js";
18
18
 
19
+ function isIconMode(m: string): m is IconMode {
20
+ return (ICON_MODES as readonly string[]).includes(m);
21
+ }
22
+
19
23
  function statePath(): string {
20
24
  return join(getAgentDir(), "pretty.json");
21
25
  }
@@ -27,9 +31,8 @@ export function loadIconMode(): IconMode | undefined {
27
31
  if (!existsSync(p)) return undefined;
28
32
  const raw = JSON.parse(readFileSync(p, "utf-8")) as { icons?: string };
29
33
  const mode = raw?.icons;
30
- return ICON_MODES.includes(mode as IconMode)
31
- ? (mode as IconMode)
32
- : undefined;
34
+ if (mode == null) return undefined;
35
+ return isIconMode(mode) ? mode : undefined;
33
36
  } catch {
34
37
  return undefined;
35
38
  }
package/src/progress.ts CHANGED
@@ -89,7 +89,7 @@ export function openProgress(
89
89
  width: mw,
90
90
  lines: [
91
91
  theme.fg(accent, theme.bold(title)),
92
- `${theme.fg(accent, SPINNER[frame])} ${theme.fg("muted", labelValue)}`,
92
+ `${theme.fg(accent, SPINNER[frame] ?? "")} ${theme.fg("muted", labelValue)}`,
93
93
  ],
94
94
  color: (s) => theme.fg(accent, s),
95
95
  bg: (s) => theme.bg("customMessageBg", s),
package/src/renderers.ts CHANGED
@@ -92,7 +92,7 @@ export function renderTree(text: string, _basePath: string): string {
92
92
  const show = lines.slice(0, MAX_PREVIEW_LINES);
93
93
 
94
94
  for (let i = 0; i < show.length; i++) {
95
- const entry = show[i].trim();
95
+ const entry = (show[i] ?? "").trim();
96
96
  const isLast = i === show.length - 1 && total <= MAX_PREVIEW_LINES;
97
97
  const prefix = isLast ? "└── " : "├── ";
98
98
  const connector = `${FG_RULE}${prefix}${RST}`;