@xynogen/pix-pretty 1.7.13 → 1.7.16

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.13",
3
+ "version": "1.7.16",
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",
@@ -58,7 +58,7 @@
58
58
  "access": "public"
59
59
  },
60
60
  "dependencies": {
61
- "@xynogen/pix-data": "workspace:*",
61
+ "@xynogen/pix-data": "^0.3.0",
62
62
  "cli-highlight": "^2.1.11",
63
63
  "@ff-labs/fff-node": "^0.5.2",
64
64
  "diff": "^7.0.0"
package/src/ansi.ts CHANGED
@@ -17,12 +17,8 @@ export let BG_BASE = BG_DEFAULT; // tool box success/base bg — updated from th
17
17
  export let BG_ERROR = BG_DEFAULT; // tool box error bg — updated from theme's toolErrorBg
18
18
 
19
19
  /** Parse an ANSI 24-bit color escape into { r, g, b }. Handles both fg (38;2) and bg (48;2). */
20
- function parseAnsiRgb(
21
- ansi: string,
22
- ): { r: number; g: number; b: number } | null {
23
- const m = ansi.match(
24
- new RegExp(`${ESC_RE}\\[(?:38|48);2;(\\d+);(\\d+);(\\d+)m`),
25
- );
20
+ function parseAnsiRgb(ansi: string): { r: number; g: number; b: number } | null {
21
+ const m = ansi.match(new RegExp(`${ESC_RE}\\[(?:38|48);2;(\\d+);(\\d+);(\\d+)m`));
26
22
  return m ? { r: +(m[1] ?? 0), g: +(m[2] ?? 0), b: +(m[3] ?? 0) } : null;
27
23
  }
28
24
 
@@ -40,10 +36,7 @@ function getThemeBgAnsi(theme: BgTheme, key: string): string | null {
40
36
  export function resolveBaseBackground(theme: BgTheme | null | undefined): void {
41
37
  if (!theme?.getBgAnsi) return;
42
38
 
43
- BG_BASE =
44
- getThemeBgAnsi(theme, "toolBg") ??
45
- getThemeBgAnsi(theme, "background") ??
46
- BG_DEFAULT;
39
+ BG_BASE = getThemeBgAnsi(theme, "toolBg") ?? getThemeBgAnsi(theme, "background") ?? BG_DEFAULT;
47
40
  BG_ERROR = getThemeBgAnsi(theme, "toolErrorBg") ?? BG_BASE;
48
41
  RST = `\x1b[0m${BG_BASE}`;
49
42
  }
@@ -61,8 +54,7 @@ function isLowContrastShikiFg(params: string): boolean {
61
54
  if (params === "38;5;0" || params === "38;5;8") return true;
62
55
  if (!params.startsWith("38;2;")) return false;
63
56
  const parts = params.split(";").map(Number);
64
- if (parts.length !== 5 || parts.some((n) => !Number.isFinite(n)))
65
- return false;
57
+ if (parts.length !== 5 || parts.some((n) => !Number.isFinite(n))) return false;
66
58
  const [, , r = 0, g = 0, b = 0] = parts;
67
59
  const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
68
60
  return luminance < 72;
@@ -93,9 +93,7 @@ export function registerPrettyCommand(pi: PiPrettyApi): void {
93
93
  const name = theme.fg(sel ? "accent" : "text", mode.padEnd(8));
94
94
  // Live preview: render the sample glyphs in THIS mode.
95
95
  const prev = ICON_MODES[i] === getIconMode();
96
- const samples = prev
97
- ? PREVIEW.map((p) => icon(p.key)).join(" ")
98
- : "";
96
+ const samples = prev ? PREVIEW.map((p) => icon(p.key)).join(" ") : "";
99
97
  return `${cursor} ${name} ${theme.fg("dim", samples)}`;
100
98
  });
101
99
  const lines = [
@@ -115,8 +113,7 @@ export function registerPrettyCommand(pi: PiPrettyApi): void {
115
113
  invalidate: () => {},
116
114
  handleInput: (data: string) => {
117
115
  if (data === "k" || data === "\u001b[A") choose(selected - 1);
118
- else if (data === "j" || data === "\u001b[B")
119
- choose(selected + 1);
116
+ else if (data === "j" || data === "\u001b[B") choose(selected + 1);
120
117
  else if (data === "\u001b" || data === "q" || data === "\r") {
121
118
  done(null);
122
119
  return;
package/src/config.ts CHANGED
@@ -16,14 +16,10 @@ function readThemeFromSettings(agentDir?: string): BundledTheme | undefined {
16
16
  if (!resolvedAgentDir) return undefined;
17
17
 
18
18
  try {
19
- const settings = JSON.parse(
20
- readFileSync(join(resolvedAgentDir, "settings.json"), "utf8"),
21
- ) as {
19
+ const settings = JSON.parse(readFileSync(join(resolvedAgentDir, "settings.json"), "utf8")) as {
22
20
  theme?: unknown;
23
21
  };
24
- return typeof settings.theme === "string"
25
- ? (settings.theme as BundledTheme)
26
- : undefined;
22
+ return typeof settings.theme === "string" ? (settings.theme as BundledTheme) : undefined;
27
23
  } catch {
28
24
  return undefined;
29
25
  }
@@ -53,11 +49,7 @@ export function envInt(name: string, fallback: number): number {
53
49
  }
54
50
 
55
51
  // Precedence for numeric config: env var → pix.json → hardcoded default
56
- function pixOrEnvInt(
57
- envName: string,
58
- pixValue: number,
59
- fallback: number,
60
- ): number {
52
+ function pixOrEnvInt(envName: string, pixValue: number, fallback: number): number {
61
53
  const env = process.env[envName];
62
54
  if (env) {
63
55
  const v = Number.parseInt(env, 10);
@@ -68,30 +60,14 @@ function pixOrEnvInt(
68
60
 
69
61
  const pc = pixConfig().pretty;
70
62
 
71
- export const MAX_HL_CHARS = pixOrEnvInt(
72
- "PRETTY_MAX_HL_CHARS",
73
- pc.maxHighlightChars,
74
- 80_000,
75
- );
63
+ export const MAX_HL_CHARS = pixOrEnvInt("PRETTY_MAX_HL_CHARS", pc.maxHighlightChars, 80_000);
76
64
 
77
- export const MAX_PREVIEW_LINES = pixOrEnvInt(
78
- "PRETTY_MAX_PREVIEW_LINES",
79
- pc.maxPreviewLines,
80
- 80,
81
- );
65
+ export const MAX_PREVIEW_LINES = pixOrEnvInt("PRETTY_MAX_PREVIEW_LINES", pc.maxPreviewLines, 80);
82
66
 
83
- export const CACHE_LIMIT = pixOrEnvInt(
84
- "PRETTY_CACHE_LIMIT",
85
- pc.cacheLimit,
86
- 128,
87
- );
67
+ export const CACHE_LIMIT = pixOrEnvInt("PRETTY_CACHE_LIMIT", pc.cacheLimit, 128);
88
68
 
89
69
  // --- Diff rendering limits (edit/write tools) ---
90
- export const MAX_RENDER_LINES = pixOrEnvInt(
91
- "PRETTY_MAX_RENDER_LINES",
92
- pc.maxRenderLines,
93
- 150,
94
- );
70
+ export const MAX_RENDER_LINES = pixOrEnvInt("PRETTY_MAX_RENDER_LINES", pc.maxRenderLines, 150);
95
71
 
96
72
  // Word-level emphasis only when paired del/add lines are at least this similar.
97
73
  export const WORD_DIFF_MIN_SIM = 0.15;
package/src/confirm.ts CHANGED
@@ -55,19 +55,13 @@ const COUNTDOWN_WARN_S = 5;
55
55
  /**
56
56
  * Show a Yes/No overlay. Resolves true on confirm, false otherwise.
57
57
  */
58
- export function confirmOverlay(
59
- ui: ConfirmUI,
60
- opts: ConfirmOptions,
61
- ): Promise<boolean> {
58
+ export function confirmOverlay(ui: ConfirmUI, opts: ConfirmOptions): Promise<boolean> {
62
59
  const accent = opts.accent ?? "accent";
63
60
  const timeoutMs = opts.timeoutMs ?? 0;
64
61
 
65
62
  return new Promise((resolve) => {
66
63
  const controller = new AbortController();
67
- const timer =
68
- timeoutMs > 0
69
- ? setTimeout(() => controller.abort(), timeoutMs)
70
- : undefined;
64
+ const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : undefined;
71
65
 
72
66
  ui.custom<boolean>(
73
67
  (tui, theme, _kb, done) => {
@@ -87,25 +81,15 @@ export function confirmOverlay(
87
81
  },
88
82
  ];
89
83
 
90
- const selectList = new SelectList(
91
- choices,
92
- choices.length,
93
- selectListTheme(theme, accent),
94
- );
84
+ const selectList = new SelectList(choices, choices.length, selectListTheme(theme, accent));
95
85
 
96
86
  if (timeoutMs > 0) {
97
87
  const deadlineMs = Date.now() + timeoutMs;
98
88
  const updateCountdown = () => {
99
- const remaining = Math.max(
100
- 0,
101
- Math.ceil((deadlineMs - Date.now()) / SECOND_MS),
102
- );
89
+ const remaining = Math.max(0, Math.ceil((deadlineMs - Date.now()) / SECOND_MS));
103
90
  countdownLine =
104
91
  theme.fg("dim", "Auto-cancel in ") +
105
- theme.fg(
106
- remaining <= COUNTDOWN_WARN_S ? accent : "muted",
107
- `${remaining}s`,
108
- );
92
+ theme.fg(remaining <= COUNTDOWN_WARN_S ? accent : "muted", `${remaining}s`);
109
93
  };
110
94
  updateCountdown();
111
95
  ticker = setInterval(() => {
@@ -148,9 +132,7 @@ export function confirmOverlay(
148
132
  for (const l of selectList.render(inner)) lines.push(l);
149
133
 
150
134
  lines.push("");
151
- lines.push(
152
- theme.fg("dim", "↑↓ navigate • enter select • esc cancel"),
153
- );
135
+ lines.push(theme.fg("dim", "↑↓ navigate • enter select • esc cancel"));
154
136
 
155
137
  return frameLines({
156
138
  width: mw,
@@ -72,31 +72,13 @@ const dc = pixConfig().pretty.diff;
72
72
  // Precedence: env → pix.json → hardcoded default
73
73
  const BG_ADD = envBg("DIFF_BG_ADD", hexToBg(dc.bgAdd) || "\x1b[48;2;22;38;32m");
74
74
  const BG_DEL = envBg("DIFF_BG_DEL", hexToBg(dc.bgDel) || "\x1b[48;2;45;25;25m");
75
- const BG_ADD_W = envBg(
76
- "DIFF_BG_ADD_HL",
77
- hexToBg(dc.bgAddHighlight) || "\x1b[48;2;35;75;50m",
78
- );
79
- const BG_DEL_W = envBg(
80
- "DIFF_BG_DEL_HL",
81
- hexToBg(dc.bgDelHighlight) || "\x1b[48;2;80;35;35m",
82
- );
83
- const BG_GUTTER_ADD = envBg(
84
- "DIFF_BG_GUTTER_ADD",
85
- hexToBg(dc.bgGutterAdd) || "\x1b[48;2;18;32;26m",
86
- );
87
- const BG_GUTTER_DEL = envBg(
88
- "DIFF_BG_GUTTER_DEL",
89
- hexToBg(dc.bgGutterDel) || "\x1b[48;2;38;22;22m",
90
- );
91
-
92
- const FG_ADD = envFg(
93
- "DIFF_FG_ADD",
94
- hexToFg(dc.fgAdd) || "\x1b[38;2;100;180;120m",
95
- );
96
- const FG_DEL = envFg(
97
- "DIFF_FG_DEL",
98
- hexToFg(dc.fgDel) || "\x1b[38;2;200;100;100m",
99
- );
75
+ const BG_ADD_W = envBg("DIFF_BG_ADD_HL", hexToBg(dc.bgAddHighlight) || "\x1b[48;2;35;75;50m");
76
+ const BG_DEL_W = envBg("DIFF_BG_DEL_HL", hexToBg(dc.bgDelHighlight) || "\x1b[48;2;80;35;35m");
77
+ const BG_GUTTER_ADD = envBg("DIFF_BG_GUTTER_ADD", hexToBg(dc.bgGutterAdd) || "\x1b[48;2;18;32;26m");
78
+ const BG_GUTTER_DEL = envBg("DIFF_BG_GUTTER_DEL", hexToBg(dc.bgGutterDel) || "\x1b[48;2;38;22;22m");
79
+
80
+ const FG_ADD = envFg("DIFF_FG_ADD", hexToFg(dc.fgAdd) || "\x1b[38;2;100;180;120m");
81
+ const FG_DEL = envFg("DIFF_FG_DEL", hexToFg(dc.fgDel) || "\x1b[38;2;200;100;100m");
100
82
  const FG_STRIPE = "\x1b[38;2;40;40;40m"; // diagonal stripes on filler cells
101
83
 
102
84
  const BORDER_BAR = "▌";
@@ -115,10 +97,7 @@ const MAX_TERM_WIDTH = 210;
115
97
  const MAX_PREVIEW_LINES = envInt("PRETTY_MAX_PREVIEW_LINES", 80);
116
98
 
117
99
  const SPLIT_MIN_WIDTH = envInt("DIFF_SPLIT_MIN_WIDTH", dc.splitMinWidth || 150);
118
- const SPLIT_MIN_CODE_WIDTH = envInt(
119
- "DIFF_SPLIT_MIN_CODE_WIDTH",
120
- dc.splitMinCodeWidth || 60,
121
- );
100
+ const SPLIT_MIN_CODE_WIDTH = envInt("DIFF_SPLIT_MIN_CODE_WIDTH", dc.splitMinCodeWidth || 60);
122
101
  const SPLIT_MAX_WRAP_RATIO = 0.2;
123
102
  const SPLIT_MAX_WRAP_LINES = 8;
124
103
 
@@ -192,20 +171,12 @@ function ensureContrast(fg: string, bgSeq: string, min = 3): string {
192
171
  *
193
172
  * Theme hue is preserved, but each add/del fg is contrast-checked against the
194
173
  * gutter bg it is painted on and lifted if it would render too dark to read. */
195
- export function resolveDiffColors(theme?: {
196
- getFgAnsi?: (key: string) => string;
197
- }): DiffColors {
174
+ export function resolveDiffColors(theme?: { getFgAnsi?: (key: string) => string }): DiffColors {
198
175
  if (!theme?.getFgAnsi) return DEFAULT_DIFF_COLORS;
199
176
  try {
200
177
  return {
201
- fgAdd: ensureContrast(
202
- theme.getFgAnsi("toolDiffAdded") || FG_ADD,
203
- BG_GUTTER_ADD,
204
- ),
205
- fgDel: ensureContrast(
206
- theme.getFgAnsi("toolDiffRemoved") || FG_DEL,
207
- BG_GUTTER_DEL,
208
- ),
178
+ fgAdd: ensureContrast(theme.getFgAnsi("toolDiffAdded") || FG_ADD, BG_GUTTER_ADD),
179
+ fgDel: ensureContrast(theme.getFgAnsi("toolDiffRemoved") || FG_DEL, BG_GUTTER_DEL),
209
180
  fgCtx: theme.getFgAnsi("toolDiffContext") || FG_DIM,
210
181
  };
211
182
  } catch {
@@ -214,9 +185,7 @@ export function resolveDiffColors(theme?: {
214
185
  }
215
186
 
216
187
  /** Stable cache key for the resolved diff theme colors. */
217
- export function diffThemeCacheKey(theme?: {
218
- getFgAnsi?: (key: string) => string;
219
- }): string {
188
+ export function diffThemeCacheKey(theme?: { getFgAnsi?: (key: string) => string }): string {
220
189
  const c = resolveDiffColors(theme);
221
190
  return `${c.fgAdd}|${c.fgDel}|${c.fgCtx}|${BG_BASE}`;
222
191
  }
@@ -266,9 +235,7 @@ function fit(s: string, w: number): string {
266
235
  vis++;
267
236
  i++;
268
237
  }
269
- return w > 2
270
- ? `${s.slice(0, i)}${RST}${FG_DIM}›${RST}`
271
- : `${s.slice(0, i)}${RST}`;
238
+ return w > 2 ? `${s.slice(0, i)}${RST}${FG_DIM}›${RST}` : `${s.slice(0, i)}${RST}`;
272
239
  }
273
240
 
274
241
  /** Extract last active fg + bg ANSI codes from a string (for wrap continuations). */
@@ -293,12 +260,7 @@ function ansiState(s: string): string {
293
260
  }
294
261
 
295
262
  /** Wrap ANSI-encoded string into rows of `w` visible chars. */
296
- function wrapAnsi(
297
- s: string,
298
- w: number,
299
- maxRows = adaptiveWrapRows(),
300
- fillBg = "",
301
- ): string[] {
263
+ function wrapAnsi(s: string, w: number, maxRows = adaptiveWrapRows(), fillBg = ""): string[] {
302
264
  if (w <= 0) return [""];
303
265
  const plain = strip(s);
304
266
  if (plain.length <= w) {
@@ -375,12 +337,7 @@ function stripes(w: number, _rowOffset: number): string {
375
337
 
376
338
  /** Right-aligned line number. `noReset` keeps the active bg alive (no
377
339
  * trailing RST) so the caller can build one bg-continuous gutter segment. */
378
- function lnum(
379
- n: number | null,
380
- w: number,
381
- fg = FG_LNUM,
382
- noReset = false,
383
- ): string {
340
+ function lnum(n: number | null, w: number, fg = FG_LNUM, noReset = false): string {
384
341
  if (n === null) return " ".repeat(w);
385
342
  const v = String(n);
386
343
  const pad = " ".repeat(Math.max(0, w - v.length));
@@ -402,20 +359,8 @@ function buildGutter(opts: {
402
359
  nw: number;
403
360
  continuation: boolean;
404
361
  }): string {
405
- const {
406
- borderFg,
407
- gutterBg,
408
- bodyBg,
409
- num,
410
- numFg,
411
- signFg,
412
- sign,
413
- nw,
414
- continuation,
415
- } = opts;
416
- const border = borderFg
417
- ? `${gutterBg}${borderFg}${BORDER_BAR}`
418
- : `${BG_BASE} `;
362
+ const { borderFg, gutterBg, bodyBg, num, numFg, signFg, sign, nw, continuation } = opts;
363
+ const border = borderFg ? `${gutterBg}${borderFg}${BORDER_BAR}` : `${BG_BASE} `;
419
364
  const numCell = continuation
420
365
  ? " ".repeat(nw + 2)
421
366
  : `${lnum(num, nw, numFg, true)}${signFg}${sign} `;
@@ -505,8 +450,7 @@ function injectBg(
505
450
  continue;
506
451
  }
507
452
  }
508
- while (ri < ranges.length && vis >= (ranges[ri] as [number, number])[1])
509
- ri++;
453
+ while (ri < ranges.length && vis >= (ranges[ri] as [number, number])[1]) ri++;
510
454
  const want =
511
455
  ri < ranges.length &&
512
456
  vis >= (ranges[ri] as [number, number])[0] &&
@@ -523,10 +467,7 @@ function injectBg(
523
467
  }
524
468
 
525
469
  /** Simple word diff (no syntax hl) — fallback when highlighting is unavailable. */
526
- function plainWordDiff(
527
- oldText: string,
528
- newText: string,
529
- ): { old: string; new: string } {
470
+ function plainWordDiff(oldText: string, newText: string): { old: string; new: string } {
530
471
  const parts = Diff.diffWords(oldText, newText);
531
472
  let o = "";
532
473
  let n = "";
@@ -551,18 +492,13 @@ function at<T>(arr: T[], i: number): T {
551
492
  // Split-vs-unified decision
552
493
  // ---------------------------------------------------------------------------
553
494
 
554
- function shouldUseSplit(
555
- diff: ParsedDiff,
556
- tw: number,
557
- maxRows = MAX_PREVIEW_LINES,
558
- ): boolean {
495
+ function shouldUseSplit(diff: ParsedDiff, tw: number, maxRows = MAX_PREVIEW_LINES): boolean {
559
496
  if (!diff.lines.length) return false;
560
497
  if (tw < SPLIT_MIN_WIDTH) return false;
561
498
 
562
499
  const nw = Math.max(
563
500
  2,
564
- String(Math.max(...diff.lines.map((l) => l.oldNum ?? l.newNum ?? 0), 0))
565
- .length,
501
+ String(Math.max(...diff.lines.map((l) => l.oldNum ?? l.newNum ?? 0), 0)).length,
566
502
  );
567
503
  const half = Math.floor((tw - 1) / 2);
568
504
  const gw = nw + 5;
@@ -599,10 +535,7 @@ export async function renderUnified(
599
535
 
600
536
  const vis = diff.lines.slice(0, max);
601
537
  const tw = termW();
602
- const nw = Math.max(
603
- 2,
604
- String(Math.max(...vis.map((l) => l.oldNum ?? l.newNum ?? 0), 0)).length,
605
- );
538
+ const nw = Math.max(2, String(Math.max(...vis.map((l) => l.oldNum ?? l.newNum ?? 0), 0)).length);
606
539
  const gw = nw + 5;
607
540
  const cw = Math.max(20, tw - gw);
608
541
  const canHL = diff.chars <= MAX_HL_CHARS && vis.length <= MAX_RENDER_LINES;
@@ -650,8 +583,7 @@ export async function renderUnified(
650
583
  const contGutter = buildGutter({ ...gutterArgs, continuation: true });
651
584
  const rows = wrapAnsi(tabs(body), cw, adaptiveWrapRows(), bodyBg);
652
585
  out.push(`${gutter}${rows[0]}${RST}`);
653
- for (let r = 1; r < rows.length; r++)
654
- out.push(`${contGutter}${rows[r]}${RST}`);
586
+ for (let r = 1; r < rows.length; r++) out.push(`${contGutter}${rows[r]}${RST}`);
655
587
  }
656
588
 
657
589
  while (idx < vis.length) {
@@ -664,23 +596,14 @@ export async function renderUnified(
664
596
  const pad = Math.max(0, totalW - label.length - 2);
665
597
  const half1 = Math.floor(pad / 2);
666
598
  const half2 = pad - half1;
667
- out.push(
668
- `${BG_BASE}${FG_DIM}${"─".repeat(half1)}${label}${"─".repeat(half2)}${RST}`,
669
- );
599
+ out.push(`${BG_BASE}${FG_DIM}${"─".repeat(half1)}${label}${"─".repeat(half2)}${RST}`);
670
600
  idx++;
671
601
  continue;
672
602
  }
673
603
 
674
604
  if (l.type === "ctx") {
675
605
  const hl = oldHL[oI] ?? l.content;
676
- emitRow(
677
- l.newNum,
678
- " ",
679
- BG_BASE,
680
- dc.fgCtx,
681
- `${BG_BASE}${DIM}${hl}`,
682
- BG_BASE,
683
- );
606
+ emitRow(l.newNum, " ", BG_BASE, dc.fgCtx, `${BG_BASE}${DIM}${hl}`, BG_BASE);
684
607
  oI++;
685
608
  nI++;
686
609
  idx++;
@@ -705,9 +628,7 @@ export async function renderUnified(
705
628
  }
706
629
 
707
630
  const isPaired = dels.length === 1 && adds.length === 1;
708
- const wd = isPaired
709
- ? wordDiffAnalysis(at(dels, 0).l.content, at(adds, 0).l.content)
710
- : null;
631
+ const wd = isPaired ? wordDiffAnalysis(at(dels, 0).l.content, at(adds, 0).l.content) : null;
711
632
  const wdBalanced = wd && wd.oldRanges.length > 0 && wd.newRanges.length > 0;
712
633
 
713
634
  if (isPaired && wdBalanced && wd.similarity >= WORD_DIFF_MIN_SIM && canHL) {
@@ -715,30 +636,11 @@ export async function renderUnified(
715
636
  const add0 = at(adds, 0);
716
637
  const delBody = injectBg(del0.hl, wd.oldRanges, BG_DEL, BG_DEL_W);
717
638
  const addBody = injectBg(add0.hl, wd.newRanges, BG_ADD, BG_ADD_W);
718
- emitRow(
719
- del0.l.oldNum,
720
- "-",
721
- BG_GUTTER_DEL,
722
- `${dc.fgDel}${BOLD}`,
723
- delBody,
724
- BG_DEL,
725
- );
726
- emitRow(
727
- add0.l.newNum,
728
- "+",
729
- BG_GUTTER_ADD,
730
- `${dc.fgAdd}${BOLD}`,
731
- addBody,
732
- BG_ADD,
733
- );
639
+ emitRow(del0.l.oldNum, "-", BG_GUTTER_DEL, `${dc.fgDel}${BOLD}`, delBody, BG_DEL);
640
+ emitRow(add0.l.newNum, "+", BG_GUTTER_ADD, `${dc.fgAdd}${BOLD}`, addBody, BG_ADD);
734
641
  continue;
735
642
  }
736
- if (
737
- isPaired &&
738
- wdBalanced &&
739
- wd.similarity >= WORD_DIFF_MIN_SIM &&
740
- !canHL
741
- ) {
643
+ if (isPaired && wdBalanced && wd.similarity >= WORD_DIFF_MIN_SIM && !canHL) {
742
644
  const del0 = at(dels, 0);
743
645
  const add0 = at(adds, 0);
744
646
  const pwd = plainWordDiff(del0.l.content, add0.l.content);
@@ -763,33 +665,17 @@ export async function renderUnified(
763
665
 
764
666
  for (const d of dels) {
765
667
  const body = canHL ? `${BG_DEL}${d.hl}` : `${BG_DEL}${d.l.content}`;
766
- emitRow(
767
- d.l.oldNum,
768
- "-",
769
- BG_GUTTER_DEL,
770
- `${dc.fgDel}${BOLD}`,
771
- body,
772
- BG_DEL,
773
- );
668
+ emitRow(d.l.oldNum, "-", BG_GUTTER_DEL, `${dc.fgDel}${BOLD}`, body, BG_DEL);
774
669
  }
775
670
  for (const a of adds) {
776
671
  const body = canHL ? `${BG_ADD}${a.hl}` : `${BG_ADD}${a.l.content}`;
777
- emitRow(
778
- a.l.newNum,
779
- "+",
780
- BG_GUTTER_ADD,
781
- `${dc.fgAdd}${BOLD}`,
782
- body,
783
- BG_ADD,
784
- );
672
+ emitRow(a.l.newNum, "+", BG_GUTTER_ADD, `${dc.fgAdd}${BOLD}`, body, BG_ADD);
785
673
  }
786
674
  }
787
675
 
788
676
  out.push(rule(tw));
789
677
  if (diff.lines.length > vis.length) {
790
- out.push(
791
- `${BG_BASE}${FG_DIM} … ${diff.lines.length - vis.length} more lines${RST}`,
792
- );
678
+ out.push(`${BG_BASE}${FG_DIM} … ${diff.lines.length - vis.length} more lines${RST}`);
793
679
  }
794
680
  return out.join("\n");
795
681
  }
@@ -805,8 +691,7 @@ export async function renderSplit(
805
691
  dc: DiffColors = DEFAULT_DIFF_COLORS,
806
692
  ): Promise<string> {
807
693
  const tw = termW();
808
- if (!shouldUseSplit(diff, tw, max))
809
- return renderUnified(diff, language, max, dc);
694
+ if (!shouldUseSplit(diff, tw, max)) return renderUnified(diff, language, max, dc);
810
695
  if (!diff.lines.length) return "";
811
696
 
812
697
  type Row = { left: DiffLine | null; right: DiffLine | null };
@@ -834,21 +719,18 @@ export async function renderSplit(
834
719
  i++;
835
720
  }
836
721
  const n = Math.max(dels.length, adds.length);
837
- for (let j = 0; j < n; j++)
838
- rows.push({ left: dels[j] ?? null, right: adds[j] ?? null });
722
+ for (let j = 0; j < n; j++) rows.push({ left: dels[j] ?? null, right: adds[j] ?? null });
839
723
  }
840
724
 
841
725
  const vis = rows.slice(0, max);
842
726
  const half = Math.floor((tw - 1) / 2);
843
727
  const nw = Math.max(
844
728
  2,
845
- String(Math.max(...diff.lines.map((l) => l.oldNum ?? l.newNum ?? 0), 0))
846
- .length,
729
+ String(Math.max(...diff.lines.map((l) => l.oldNum ?? l.newNum ?? 0), 0)).length,
847
730
  );
848
731
  const gw = nw + 5;
849
732
  const cw = Math.max(12, half - gw);
850
- const canHL =
851
- diff.chars <= MAX_HL_CHARS && vis.length * 2 <= MAX_RENDER_LINES * 2;
733
+ const canHL = diff.chars <= MAX_HL_CHARS && vis.length * 2 <= MAX_RENDER_LINES * 2;
852
734
 
853
735
  const leftSrc: string[] = [];
854
736
  const rightSrc: string[] = [];
@@ -920,9 +802,7 @@ export async function renderSplit(
920
802
 
921
803
  // Split view's non-bordered context rows lead with a space before bg;
922
804
  // buildGutter handles bordered rows, so feed the same border convention.
923
- const splitBorder = borderFg
924
- ? `${gBg}${borderFg}${BORDER_BAR}`
925
- : ` ${BG_BASE}`;
805
+ const splitBorder = borderFg ? `${gBg}${borderFg}${BORDER_BAR}` : ` ${BG_BASE}`;
926
806
  const numCell = `${lnum(num, nw, numFg, true)}${sFg}${BOLD}${sign} `;
927
807
  const gutter = `${splitBorder}${gBg}${numCell}${FG_RULE}│${cBg} ${RST}`;
928
808
  const contGutter = `${splitBorder}${gBg}${" ".repeat(nw + 2)}${FG_RULE}│${cBg} ${RST}`;
@@ -936,14 +816,8 @@ export async function renderSplit(
936
816
  for (const r of vis) {
937
817
  const leftLine = r.left;
938
818
  const rightLine = r.right;
939
- const paired =
940
- leftLine &&
941
- rightLine &&
942
- leftLine.type === "del" &&
943
- rightLine.type === "add";
944
- const wd = paired
945
- ? wordDiffAnalysis(leftLine.content, rightLine.content)
946
- : null;
819
+ const paired = leftLine && rightLine && leftLine.type === "del" && rightLine.type === "add";
820
+ const wd = paired ? wordDiffAnalysis(leftLine.content, rightLine.content) : null;
947
821
 
948
822
  let lResult: HalfResult;
949
823
  let rResult: HalfResult;
@@ -961,13 +835,9 @@ export async function renderSplit(
961
835
  rResult = halfBuild(rightLine, pwd.new, null, "right");
962
836
  } else {
963
837
  const lhl =
964
- leftLine && leftLine.type !== "sep"
965
- ? (leftHL[lI++] ?? leftLine?.content ?? "")
966
- : "";
838
+ leftLine && leftLine.type !== "sep" ? (leftHL[lI++] ?? leftLine?.content ?? "") : "";
967
839
  const rhl =
968
- rightLine && rightLine.type !== "sep"
969
- ? (rightHL[rI++] ?? rightLine?.content ?? "")
970
- : "";
840
+ rightLine && rightLine.type !== "sep" ? (rightHL[rI++] ?? rightLine?.content ?? "") : "";
971
841
  lResult = halfBuild(leftLine, lhl, null, "left");
972
842
  rResult = halfBuild(rightLine, rhl, null, "right");
973
843
  }
@@ -987,9 +857,7 @@ export async function renderSplit(
987
857
 
988
858
  out.push(`${rule(half)}${FG_RULE}┊${RST}${rule(half)}`);
989
859
  if (rows.length > vis.length) {
990
- out.push(
991
- `${BG_BASE}${FG_DIM} … ${rows.length - vis.length} more lines${RST}`,
992
- );
860
+ out.push(`${BG_BASE}${FG_DIM} … ${rows.length - vis.length} more lines${RST}`);
993
861
  }
994
862
  return out.join("\n");
995
863
  }
package/src/diff.ts CHANGED
@@ -42,8 +42,7 @@ export function parseDiff(
42
42
  for (let hi = 0; hi < patch.hunks.length; hi++) {
43
43
  if (hi > 0) {
44
44
  const prev = at(patch.hunks, hi - 1);
45
- const gap =
46
- at(patch.hunks, hi).oldStart - (prev.oldStart + prev.oldLines);
45
+ const gap = at(patch.hunks, hi).oldStart - (prev.oldStart + prev.oldLines);
47
46
  lines.push({
48
47
  type: "sep",
49
48
  oldNum: null,
package/src/fff.ts CHANGED
@@ -31,9 +31,7 @@ export function getPiPrettyFffDir(_agentDir: string): string {
31
31
  return join(cacheHome, "pi", "fff");
32
32
  }
33
33
 
34
- export async function fffEnsureFinder(
35
- cwd: string,
36
- ): Promise<FffBackedFinder | null> {
34
+ export async function fffEnsureFinder(cwd: string): Promise<FffBackedFinder | null> {
37
35
  if (fffState.finder && !fffState.finder.isDestroyed) return fffState.finder;
38
36
  if (!fffState.module || !fffState.dbDir) return null;
39
37
 
@@ -68,13 +66,9 @@ export function fffDestroy(): void {
68
66
  function sanitizeGrepRecordContent(text: string): string {
69
67
  let content = text;
70
68
  if (content.endsWith("\r\n")) content = content.slice(0, -2);
71
- else if (content.endsWith("\r") || content.endsWith("\n"))
72
- content = content.slice(0, -1);
69
+ else if (content.endsWith("\r") || content.endsWith("\n")) content = content.slice(0, -1);
73
70
 
74
- return content
75
- .replace(/\r\n/g, "\\n")
76
- .replace(/\r/g, "\\r")
77
- .replace(/\n/g, "\\n");
71
+ return content.replace(/\r\n/g, "\\n").replace(/\r/g, "\\r").replace(/\n/g, "\\n");
78
72
  }
79
73
 
80
74
  function truncateGrepRecordContent(text: string): string {
@@ -27,9 +27,7 @@ interface Wired {
27
27
  * the SelectList keys; simpler: we expose the live component so the test can
28
28
  * call its handlers. We do the latter via the captured component ref.
29
29
  */
30
- function makeUI(
31
- onReady: (comp: Wired, finish: (v: unknown) => void) => void,
32
- ): OverlayUI {
30
+ function makeUI(onReady: (comp: Wired, finish: (v: unknown) => void) => void): OverlayUI {
33
31
  return {
34
32
  custom: async <T>(
35
33
  cb: (
@@ -195,9 +193,7 @@ function makeTimerUI(onReady?: (comp: Wired) => void): OverlayUI {
195
193
  ) => Wired,
196
194
  ): Promise<T | undefined> =>
197
195
  new Promise((resolve) => {
198
- const comp = cb({ requestRender: () => {} }, theme, undefined, (v) =>
199
- resolve(v),
200
- );
196
+ const comp = cb({ requestRender: () => {} }, theme, undefined, (v) => resolve(v));
201
197
  comp.render(80);
202
198
  onReady?.(comp);
203
199
  }),
@@ -14,12 +14,7 @@
14
14
  * - Single source of truth for the overlay look across pix-gate and pix-sudo.
15
15
  */
16
16
 
17
- import {
18
- Input,
19
- type SelectItem,
20
- SelectList,
21
- wrapTextWithAnsi,
22
- } from "@earendil-works/pi-tui";
17
+ import { Input, type SelectItem, SelectList, wrapTextWithAnsi } from "@earendil-works/pi-tui";
23
18
  import { frameLines, modalWidth, selectListTheme } from "./modal-frame.js";
24
19
 
25
20
  // ── Types ─────────────────────────────────────────────────────────────────────
@@ -135,16 +130,7 @@ function buildLines(opts: {
135
130
  countdownLine: string | undefined;
136
131
  width: number;
137
132
  }): string[] {
138
- const {
139
- theme,
140
- accent,
141
- config,
142
- stage,
143
- selectList,
144
- maskedInput,
145
- countdownLine,
146
- width,
147
- } = opts;
133
+ const { theme, accent, config, stage, selectList, maskedInput, countdownLine, width } = opts;
148
134
  const inner = width - 4; // CHROME = 2 border + 2 padding
149
135
  const lines: string[] = [];
150
136
 
@@ -172,10 +158,7 @@ function buildLines(opts: {
172
158
  lines.push("");
173
159
  lines.push(theme.fg("dim", "↑↓ navigate • enter select • esc deny"));
174
160
  } else {
175
- const label =
176
- config.mode === "sudo"
177
- ? (config.passwordLabel ?? "Sudo password:")
178
- : "Password:";
161
+ const label = config.mode === "sudo" ? (config.passwordLabel ?? "Sudo password:") : "Password:";
179
162
  lines.push(theme.fg("muted", label));
180
163
  const inputLines = maskedInput.render(inner);
181
164
  for (const l of inputLines) lines.push(l);
@@ -217,10 +200,7 @@ function buildLines(opts: {
217
200
  * if (result.action === "approved") runWithSudo(cmd, result.password!);
218
201
  * ```
219
202
  */
220
- export function showOverlay(
221
- ui: OverlayUI,
222
- config: OverlayConfig,
223
- ): Promise<OverlayResult> {
203
+ export function showOverlay(ui: OverlayUI, config: OverlayConfig): Promise<OverlayResult> {
224
204
  const accent = config.accent ?? "accent";
225
205
  const choices = config.choices ?? DEFAULT_CHOICES;
226
206
  const approveVal = config.approveValue ?? "yes";
@@ -291,8 +271,7 @@ export function showOverlay(
291
271
  };
292
272
  selectList.onCancel = () => finish({ action: "denied" });
293
273
 
294
- maskedInput.onSubmit = (pw) =>
295
- finish({ action: "approved", password: pw });
274
+ maskedInput.onSubmit = (pw) => finish({ action: "approved", password: pw });
296
275
  maskedInput.onEscape = () => finish({ action: "denied" });
297
276
 
298
277
  // ── component interface ──────────────────────────────────────────
package/src/highlight.ts CHANGED
@@ -10,10 +10,7 @@ import type { BundledLanguage } from "./types.js";
10
10
  // is not the process stdout chalk inspects) we default FORCE_COLOR before chalk
11
11
  // initializes, and lazy-load cli-highlight so this runs first. Respect an
12
12
  // explicit FORCE_COLOR/NO_COLOR if the user set one.
13
- if (
14
- process.env.FORCE_COLOR === undefined &&
15
- process.env.NO_COLOR === undefined
16
- ) {
13
+ if (process.env.FORCE_COLOR === undefined && process.env.NO_COLOR === undefined) {
17
14
  process.env.FORCE_COLOR = "3";
18
15
  }
19
16
 
@@ -77,6 +77,9 @@ const CATALOG = {
77
77
 
78
78
  // ── subagent widget (pix-subagent) ────────────────────────────────────
79
79
  agent: { nerd: "\u{F0BA0}", unicode: `\u2699${VS}`, ascii: "@" },
80
+ turns: { nerd: "\u{F006A}", unicode: `\u21BB${VS}`, ascii: "~" },
81
+ tools: { nerd: "\u{F1064}", unicode: `\u2692${VS}`, ascii: "T" },
82
+ tokens: { nerd: "\u{F027F}", unicode: `\u25A4${VS}`, ascii: "tk" },
80
83
  } as const;
81
84
 
82
85
  /** Every valid semantic icon key. */
@@ -47,10 +47,7 @@ export function saveIconMode(mode: IconMode): void {
47
47
  let existing: Record<string, unknown> = {};
48
48
  if (existsSync(p)) {
49
49
  try {
50
- existing = JSON.parse(readFileSync(p, "utf-8")) as Record<
51
- string,
52
- unknown
53
- >;
50
+ existing = JSON.parse(readFileSync(p, "utf-8")) as Record<string, unknown>;
54
51
  } catch {
55
52
  existing = {};
56
53
  }
@@ -71,8 +71,7 @@ export function frameLines(opts: FrameOptions): string[] {
71
71
 
72
72
  const row = (content: string): string => {
73
73
  const pad = inner - visibleWidth(content);
74
- const padded =
75
- pad > 0 ? content + " ".repeat(pad) : truncateToWidth(content, inner);
74
+ const padded = pad > 0 ? content + " ".repeat(pad) : truncateToWidth(content, inner);
76
75
  return bg(`${color("│")} ${reassert(padded)} ${color("│")}`);
77
76
  };
78
77
 
@@ -101,10 +100,7 @@ interface FgTheme {
101
100
  * Canonical SelectList theme for interactive overlays.
102
101
  * accent = active/selected, muted = descriptions, dim = scroll/hints, warning = no-match.
103
102
  */
104
- export function selectListTheme(
105
- theme: FgTheme,
106
- accent = "accent",
107
- ): SelectListThemeConfig {
103
+ export function selectListTheme(theme: FgTheme, accent = "accent"): SelectListThemeConfig {
108
104
  return {
109
105
  selectedPrefix: (t) => theme.fg(accent, t),
110
106
  selectedText: (t) => theme.fg(accent, t),
package/src/progress.ts CHANGED
@@ -55,11 +55,7 @@ const SPINNER_INTERVAL_MS = 120;
55
55
  * Open a modal progress overlay. Returns a handle to update the label and
56
56
  * close it. The overlay swallows all keystrokes until closed.
57
57
  */
58
- export function openProgress(
59
- ui: ProgressUI,
60
- title: string,
61
- accent = "accent",
62
- ): ProgressHandle {
58
+ export function openProgress(ui: ProgressUI, title: string, accent = "accent"): ProgressHandle {
63
59
  let setLabelImpl: (label: string) => void = () => {};
64
60
  let closeImpl: () => void = () => {};
65
61
 
package/src/renderers.ts CHANGED
@@ -1,15 +1,6 @@
1
1
  import { truncateToWidth } from "@earendil-works/pi-tui";
2
2
 
3
- import {
4
- BOLD,
5
- FG_BLUE,
6
- FG_DIM,
7
- FG_GREEN,
8
- FG_RED,
9
- FG_RULE,
10
- FG_YELLOW,
11
- RST,
12
- } from "./ansi.js";
3
+ import { BOLD, FG_BLUE, FG_DIM, FG_GREEN, FG_RED, FG_RULE, FG_YELLOW, RST } from "./ansi.js";
13
4
  import { MAX_PREVIEW_LINES } from "./config.js";
14
5
  import { hlBlock } from "./highlight.js";
15
6
  import { dirIcon, fileIcon } from "./icons.js";
@@ -49,9 +40,7 @@ export async function renderFileContent(
49
40
 
50
41
  out.push(rule(tw));
51
42
  if (total > maxLines) {
52
- out.push(
53
- `${FG_DIM} … ${pluralize(total - maxLines, "more line")} (${total} total)${RST}`,
54
- );
43
+ out.push(`${FG_DIM} … ${pluralize(total - maxLines, "more line")} (${total} total)${RST}`);
55
44
  }
56
45
  return out.join("\n");
57
46
  }
@@ -32,9 +32,5 @@ declare module "diff" {
32
32
  count?: number;
33
33
  }
34
34
 
35
- export function diffWords(
36
- oldStr: string,
37
- newStr: string,
38
- options?: unknown,
39
- ): Change[];
35
+ export function diffWords(oldStr: string, newStr: string, options?: unknown): Change[];
40
36
  }
package/src/types.ts CHANGED
@@ -41,28 +41,19 @@ export type ToolImageContent = ImageContent;
41
41
 
42
42
  export type ToolContent = TextContent | ImageContent;
43
43
 
44
- export type ToolResultLike<TDetails = unknown> = AgentToolResult<
45
- TDetails | undefined
46
- >;
44
+ export type ToolResultLike<TDetails = unknown> = AgentToolResult<TDetails | undefined>;
47
45
 
48
46
  type TextComponentLike = {
49
47
  setText(value: string): void;
50
48
  getText?: () => string;
51
49
  };
52
50
 
53
- export type TextComponentCtor = new (
54
- text?: string,
55
- x?: number,
56
- y?: number,
57
- ) => TextComponentLike;
51
+ export type TextComponentCtor = new (text?: string, x?: number, y?: number) => TextComponentLike;
58
52
 
59
53
  export type ThemeLike = BgTheme & FgTheme & { bold: (text: string) => string };
60
54
 
61
55
  export type RenderContextLike<
62
- TState extends Record<string, string | undefined> = Record<
63
- string,
64
- string | undefined
65
- >,
56
+ TState extends Record<string, string | undefined> = Record<string, string | undefined>,
66
57
  > = {
67
58
  lastComponent?: TextComponentLike;
68
59
  state: TState;
package/src/utils.test.ts CHANGED
@@ -49,20 +49,14 @@ describe("renderDimPreview", () => {
49
49
  });
50
50
 
51
51
  it("adds singular overflow marker for 1 extra line", () => {
52
- const body = Array.from(
53
- { length: MAX_PREVIEW_LINES + 1 },
54
- (_, i) => `L${i}`,
55
- );
52
+ const body = Array.from({ length: MAX_PREVIEW_LINES + 1 }, (_, i) => `L${i}`);
56
53
  const out = plain(renderDimPreview(body.join("\n"), theme));
57
54
  expect(out).toContain("… 1 more line");
58
55
  expect(out).not.toContain("more lines");
59
56
  });
60
57
 
61
58
  it("adds plural overflow marker for many extra lines", () => {
62
- const body = Array.from(
63
- { length: MAX_PREVIEW_LINES + 3 },
64
- (_, i) => `L${i}`,
65
- );
59
+ const body = Array.from({ length: MAX_PREVIEW_LINES + 3 }, (_, i) => `L${i}`);
66
60
  const out = plain(renderDimPreview(body.join("\n"), theme));
67
61
  expect(out).toContain("… 3 more lines");
68
62
  });
@@ -86,8 +80,6 @@ describe("renderDimPreview", () => {
86
80
  });
87
81
 
88
82
  it("does not throw on an invalid highlight regex", () => {
89
- expect(() =>
90
- renderDimPreview("text", theme, { highlight: "(" }),
91
- ).not.toThrow();
83
+ expect(() => renderDimPreview("text", theme, { highlight: "(" })).not.toThrow();
92
84
  });
93
85
  });
package/src/utils.ts CHANGED
@@ -41,21 +41,14 @@ export function fillToolBackground(text: string, bg = BG_BASE): string {
41
41
  .split("\n")
42
42
  .map((line) => {
43
43
  const normalized = preserveToolBackground(line, bg);
44
- const fitted = preserveToolBackground(
45
- truncateToWidth(normalized, width, ""),
46
- bg,
47
- );
44
+ const fitted = preserveToolBackground(truncateToWidth(normalized, width, ""), bg);
48
45
  const padding = Math.max(0, width - visibleWidth(fitted));
49
46
  return `${bg}${fitted}${" ".repeat(padding)}${RST}`;
50
47
  })
51
48
  .join("\n");
52
49
  }
53
50
 
54
- export function pluralize(
55
- count: number,
56
- noun: string,
57
- plural?: string,
58
- ): string {
51
+ export function pluralize(count: number, noun: string, plural?: string): string {
59
52
  return `${count} ${count === 1 ? noun : (plural ?? `${noun}s`)}`;
60
53
  }
61
54
 
@@ -74,18 +67,12 @@ function safeHighlightRegex(pattern: string): RegExp | null {
74
67
  }
75
68
  }
76
69
 
77
- function dimLineWithHighlight(
78
- line: string,
79
- theme: FgTheme,
80
- re: RegExp | null,
81
- ): string {
70
+ function dimLineWithHighlight(line: string, theme: FgTheme, re: RegExp | null): string {
82
71
  if (!re) return theme.fg("dim", line);
83
72
  // split with capture group: odd indexes are matches
84
73
  return line
85
74
  .split(re)
86
- .map((part, i) =>
87
- i % 2 ? `${FG_GREEN}${BOLD}${part}${RST}` : theme.fg("dim", part),
88
- )
75
+ .map((part, i) => (i % 2 ? `${FG_GREEN}${BOLD}${part}${RST}` : theme.fg("dim", part)))
89
76
  .join("");
90
77
  }
91
78
 
@@ -204,15 +191,11 @@ export function humanSize(bytes: number): string {
204
191
  // Fallback: set PRETTY_ICONS=none to disable icons.
205
192
  // ---------------------------------------------------------------------------
206
193
 
207
- export function isTextContent(
208
- content: ToolContent,
209
- ): content is ToolTextContent {
194
+ export function isTextContent(content: ToolContent): content is ToolTextContent {
210
195
  return content.type === "text";
211
196
  }
212
197
 
213
- export function isImageContent(
214
- content: ToolContent,
215
- ): content is ToolImageContent {
198
+ export function isImageContent(content: ToolContent): content is ToolImageContent {
216
199
  return content.type === "image";
217
200
  }
218
201