@xynogen/pix-pretty 1.16.2 → 1.17.2

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
@@ -74,6 +74,8 @@ Configuration is read from **`~/.pi/agent/pix.json`** (the unified config file o
74
74
  "icons": "nerd", // nerd | unicode | ascii
75
75
  "maxPreviewLines": 80,
76
76
  "maxRenderLines": 150,
77
+ "maxRenderWidth": "65%", // modal frame width; percentage or columns
78
+ "maxRenderHeight": "80%", // pagination threshold; percentage or rows
77
79
  "maxHighlightChars": 80000,
78
80
  "cacheLimit": 128,
79
81
  "diff": {
@@ -84,7 +86,7 @@ Configuration is read from **`~/.pi/agent/pix.json`** (the unified config file o
84
86
  }
85
87
  ```
86
88
 
87
- Syntax highlighting, diffs, and tool surfaces use the active Pi theme. Color overrides do not live in `pix.json`.
89
+ Syntax highlighting, diffs, and tool surfaces use the active Pi theme. Color overrides do not live in `pix.json`. `/pix` exposes modal width and height under **Pretty**; percentage choices move in 5% steps.
88
90
 
89
91
  ### Environment Variables (override `pix.json`)
90
92
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-pretty",
3
- "version": "1.16.2",
3
+ "version": "1.17.2",
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",
@@ -59,8 +59,7 @@
59
59
  "access": "public"
60
60
  },
61
61
  "dependencies": {
62
- "@xynogen/pix-data": "^0.4.3",
63
- "@xynogen/pix-runtime": "^0.6.0",
62
+ "@xynogen/pix-runtime": "^0.8.0",
64
63
  "chalk": "^4.1.2",
65
64
  "cli-highlight": "^2.1.11",
66
65
  "@ff-labs/fff-node": "^0.5.2",
package/src/confirm.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  frameModal,
11
11
  MIN_PERMISSION_MODAL_HEIGHT,
12
12
  ModalPager,
13
+ modalOverlayOptions,
13
14
  modalWidth,
14
15
  selectListTheme,
15
16
  terminalModalHeight,
@@ -37,7 +38,7 @@ export interface ConfirmUI {
37
38
  kb: KeybindingsManager,
38
39
  done: (v: T) => void,
39
40
  ) => CustomComponent,
40
- opts?: { overlay?: boolean; overlayOptions?: { maxHeight?: number | `${number}%` } },
41
+ opts?: { overlay?: boolean; overlayOptions?: ReturnType<typeof modalOverlayOptions> },
41
42
  ): Promise<T | undefined>;
42
43
  }
43
44
 
@@ -156,7 +157,7 @@ export function confirmOverlay(ui: ConfirmUI, opts: ConfirmOptions): Promise<boo
156
157
  },
157
158
  };
158
159
  },
159
- { overlay: true, overlayOptions: { maxHeight: "80%" } },
160
+ { overlay: true, overlayOptions: modalOverlayOptions() },
160
161
  ).then((result) => {
161
162
  if (timer !== undefined) clearTimeout(timer);
162
163
  resolve(result ?? false);
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, jest, test } from "bun:test";
2
2
  import { type OverlayUI, showOverlay } from "./gate-overlay.ts";
3
+ import { modalOverlayOptions } from "./modal-frame.ts";
3
4
 
4
5
  // ── Mock host ─────────────────────────────────────────────────────────────────
5
6
  //
@@ -60,7 +61,7 @@ const ENTER = "\r";
60
61
  const DOWN = "\x1b[B";
61
62
 
62
63
  describe("showOverlay — confirm mode", () => {
63
- test("caps overlay at 80% of terminal height", async () => {
64
+ test("uses configured global overlay bounds", async () => {
64
65
  let options: unknown;
65
66
  await showOverlay(
66
67
  makeUI(
@@ -71,7 +72,7 @@ describe("showOverlay — confirm mode", () => {
71
72
  ),
72
73
  { mode: "confirm", title: "T" },
73
74
  );
74
- expect(options).toEqual({ overlay: true, overlayOptions: { maxHeight: "80%" } });
75
+ expect(options).toEqual({ overlay: true, overlayOptions: modalOverlayOptions() });
75
76
  });
76
77
 
77
78
  test("selecting the approve choice (first) returns approved", async () => {
@@ -2,8 +2,10 @@
2
2
  * pix-pretty/gate-overlay — shared permission dialog component.
3
3
  *
4
4
  * One component, two modes:
5
- * "confirm" — SelectList only. Used by pix-gate for command gating.
6
- * "sudo" — SelectList masked password input. Used by pix-sudo.
5
+ * "confirm" — SelectList only. Used by pix-gate (command gating) and pix-ssh
6
+ * (host confirm when no password is missing).
7
+ * "sudo" — SelectList → masked password input. Used by pix-sudo and pix-ssh
8
+ * (SSH login + remote sudo password entry).
7
9
  *
8
10
  * Both modes share: rounded modal frame (╭─╮╰─╯), solid bg, accent border,
9
11
  * title, body lines, optional countdown. Same visual style as pix-ask.
@@ -11,7 +13,7 @@
11
13
  * Design goals:
12
14
  * - Pure function — no side effects, no global state.
13
15
  * - Fully unit-testable: inject a mock `ui` to drive inputs deterministically.
14
- * - Single source of truth for the overlay look across pix-gate and pix-sudo.
16
+ * - Single source of truth for the overlay look across pix-gate, pix-sudo, and pix-ssh.
15
17
  */
16
18
 
17
19
  import { Input, type SelectItem, SelectList } from "@earendil-works/pi-tui";
@@ -20,6 +22,7 @@ import {
20
22
  MIN_PERMISSION_MODAL_HEIGHT,
21
23
  type ModalPageKeybindings,
22
24
  ModalPager,
25
+ modalOverlayOptions,
23
26
  modalWidth,
24
27
  selectListTheme,
25
28
  terminalModalHeight,
@@ -109,7 +112,7 @@ export interface OverlayUI {
109
112
  kb: unknown,
110
113
  done: (v: T) => void,
111
114
  ) => OverlayComponent,
112
- opts?: { overlay?: boolean; overlayOptions?: { maxHeight?: number | `${number}%` } },
115
+ opts?: { overlay?: boolean; overlayOptions?: ReturnType<typeof modalOverlayOptions> },
113
116
  ): Promise<T | undefined>;
114
117
  }
115
118
 
@@ -387,7 +390,7 @@ export function showOverlay(ui: OverlayUI, config: OverlayConfig): Promise<Overl
387
390
  },
388
391
  };
389
392
  },
390
- { overlay: true, overlayOptions: { maxHeight: "80%" } },
393
+ { overlay: true, overlayOptions: modalOverlayOptions() },
391
394
  ).then((result) => {
392
395
  resolve(result ?? { action: "denied" });
393
396
  });
package/src/index.ts CHANGED
@@ -8,13 +8,15 @@
8
8
  * UI features (paste chips, thinking blocks) live in pix-display.
9
9
  */
10
10
 
11
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
12
  import { registerFffCommands } from "./commands/fff.js";
12
13
  import { fffState } from "./fff.js";
13
14
  import { clearHighlightCache } from "./highlight.js";
14
15
  import { initIconMode } from "./icon-persist.js";
15
16
  import type { PiPrettyApi } from "./types.js";
16
17
 
17
- export default function piPrettyExtension(pi: PiPrettyApi): void {
18
+ export default function piPrettyExtension(pi: ExtensionAPI): void {
19
+ const prettyPi = pi as unknown as PiPrettyApi;
18
20
  clearHighlightCache();
19
21
 
20
22
  // ── Icon mode ───────────────────────────────────────────
@@ -25,5 +27,5 @@ export default function piPrettyExtension(pi: PiPrettyApi): void {
25
27
  // ── FFF slash commands ──────────────────────────────────────────────
26
28
  // fffState is a module-level singleton shared with pix-grep/pix-find.
27
29
  // Commands become available once pix-grep initialises the finder.
28
- registerFffCommands(pi, fffState);
30
+ registerFffCommands(prettyPi, fffState);
29
31
  }
@@ -27,8 +27,10 @@ function render(offset = 0): ModalFrameResult {
27
27
  });
28
28
  }
29
29
 
30
- test("modalHeight uses 80% without exceeding the terminal", () => {
30
+ test("modalHeight resolves configured percentages and rows without exceeding the terminal", () => {
31
31
  expect(modalHeight(40)).toBe(32);
32
+ expect(modalHeight(40, "50%")).toBe(20);
33
+ expect(modalHeight(40, 12)).toBe(12);
32
34
  expect(modalHeight(10)).toBe(8);
33
35
  expect(modalHeight(1)).toBe(1);
34
36
  });
@@ -38,12 +40,14 @@ test("modalHeight is defensive about junk row counts", () => {
38
40
  expect(modalHeight(Number.POSITIVE_INFINITY)).toBe(19);
39
41
  expect(modalHeight(0)).toBe(1);
40
42
  expect(modalHeight(-5)).toBe(1);
41
- expect(modalHeight(40, 100)).toBe(40);
43
+ expect(modalHeight(40, "100%")).toBe(40);
42
44
  });
43
45
 
44
- test("modalWidth never exceeds the available render width", () => {
45
- expect(modalWidth(200)).toBe(96);
46
- expect(modalWidth(50)).toBe(46);
46
+ test("modalWidth resolves configured percentages and columns without exceeding available width", () => {
47
+ expect(modalWidth(200)).toBe(200);
48
+ expect(modalWidth(200, "50%")).toBe(100);
49
+ expect(modalWidth(200, 88)).toBe(88);
50
+ expect(modalWidth(50)).toBe(50);
47
51
  expect(modalWidth(10)).toBe(10);
48
52
  expect(modalWidth(1)).toBe(1);
49
53
  });
@@ -16,6 +16,8 @@ import {
16
16
  visibleWidth,
17
17
  wrapTextWithAnsi,
18
18
  } from "@earendil-works/pi-tui";
19
+ import { config } from "@xynogen/pix-runtime/config";
20
+ import { prettySection, type RenderSize } from "@xynogen/pix-runtime/sections";
19
21
 
20
22
  export { truncateToWidth, visibleWidth, wrapTextWithAnsi };
21
23
 
@@ -24,18 +26,36 @@ export { truncateToWidth, visibleWidth, wrapTextWithAnsi };
24
26
  // ── Constants ─────────────────────────────────────────────────────────────────
25
27
 
26
28
  const MIN_WIDTH = 40;
27
- const MAX_WIDTH = 96;
28
- const MARGIN = 4;
29
29
  /** 2 border cols + 2 padding spaces */
30
30
  const CHROME = 4;
31
31
 
32
32
  // ── Width ─────────────────────────────────────────────────────────────────────
33
33
 
34
- /** Prefer a 40–96 column modal without exceeding the available render width. */
35
- export function modalWidth(termWidth: number): number {
34
+ function resolveRenderSize(limit: RenderSize, available: number): number {
35
+ if (typeof limit === "number") return Math.floor(limit);
36
+ return Math.floor((available * Number.parseFloat(limit)) / 100);
37
+ }
38
+
39
+ /** Resolve modal width without exceeding available render width. */
40
+ export function modalWidth(termWidth: number, limit: RenderSize = "100%"): number {
36
41
  const available = Number.isFinite(termWidth) ? Math.max(1, Math.floor(termWidth)) : MIN_WIDTH;
37
- const preferred = Math.max(MIN_WIDTH, available - MARGIN);
38
- return Math.min(MAX_WIDTH, available, preferred);
42
+ return Math.max(1, Math.min(available, resolveRenderSize(limit, available)));
43
+ }
44
+
45
+ /** Native overlay options matching shared modal size contract. */
46
+ export function modalOverlayOptions(): {
47
+ anchor: "center";
48
+ width: RenderSize;
49
+ maxHeight: RenderSize;
50
+ margin: 2;
51
+ } {
52
+ const pretty = config(prettySection);
53
+ return {
54
+ anchor: "center",
55
+ width: pretty.maxRenderWidth,
56
+ maxHeight: pretty.maxRenderHeight,
57
+ margin: 2,
58
+ };
39
59
  }
40
60
 
41
61
  // ── Frame ─────────────────────────────────────────────────────────────────────
@@ -185,25 +205,26 @@ export function frameLines(opts: FrameOptions): string[] {
185
205
 
186
206
  // ── Height ────────────────────────────────────────────────────────────────────
187
207
 
188
- export const DEFAULT_MODAL_HEIGHT_PERCENT = 80;
189
208
  /** Fail-closed floor for ordinary overlays. Compare against modalHeight(), not raw rows. */
190
209
  export const MIN_MODAL_HEIGHT = 6;
191
210
  /** Fail-closed floor for permission overlays. Compare against modalHeight(), not raw rows. */
192
211
  export const MIN_PERMISSION_MODAL_HEIGHT = 12;
193
212
 
194
- /** Rows a modal may occupy: `percent` of the terminal, never more than it has. */
195
- export function modalHeight(terminalRows: number, percent = DEFAULT_MODAL_HEIGHT_PERCENT): number {
213
+ /** Rows a modal may occupy before paging, never more than terminal has. */
214
+ export function modalHeight(
215
+ terminalRows: number,
216
+ limit: RenderSize = config(prettySection).maxRenderHeight,
217
+ ): number {
196
218
  const rows = Number.isFinite(terminalRows) ? Math.max(1, Math.floor(terminalRows)) : 24;
197
- const ratio = Math.min(100, Math.max(1, percent)) / 100;
198
- return Math.max(1, Math.min(rows, Math.floor(rows * ratio)));
219
+ return Math.max(1, Math.min(rows, resolveRenderSize(limit, rows)));
199
220
  }
200
221
 
201
- /** Current modal budget. Pass host TUI rows when available; stdout is the fallback. */
222
+ /** Current modal budget. Pass host TUI rows when available; stdout is fallback. */
202
223
  export function terminalModalHeight(
203
224
  terminalRows = process.stdout.rows ?? 24,
204
- percent = DEFAULT_MODAL_HEIGHT_PERCENT,
225
+ limit: RenderSize = config(prettySection).maxRenderHeight,
205
226
  ): number {
206
- return modalHeight(terminalRows, percent);
227
+ return modalHeight(terminalRows, limit);
207
228
  }
208
229
 
209
230
  /** Rows available for variable body content: total − borders − pinned rows. */
package/src/progress.ts CHANGED
@@ -13,7 +13,13 @@
13
13
  * p.close(); // releases input, removes overlay
14
14
  */
15
15
 
16
- import { frameModal, MIN_MODAL_HEIGHT, modalWidth, terminalModalHeight } from "./modal-frame.js";
16
+ import {
17
+ frameModal,
18
+ MIN_MODAL_HEIGHT,
19
+ modalOverlayOptions,
20
+ modalWidth,
21
+ terminalModalHeight,
22
+ } from "./modal-frame.js";
17
23
  import { SPINNER } from "./widget-format.js";
18
24
 
19
25
  interface ProgressTheme {
@@ -36,7 +42,7 @@ export interface ProgressUI {
36
42
  kb: unknown,
37
43
  done: (v: T) => void,
38
44
  ) => ProgressComponent,
39
- opts?: { overlay?: boolean; overlayOptions?: { maxHeight?: number | `${number}%` } },
45
+ opts?: { overlay?: boolean; overlayOptions?: ReturnType<typeof modalOverlayOptions> },
40
46
  ): Promise<T | undefined>;
41
47
  }
42
48
 
@@ -97,7 +103,7 @@ export function openProgress(ui: ProgressUI, title: string, accent = "accent"):
97
103
  handleInput: () => {},
98
104
  };
99
105
  },
100
- { overlay: true, overlayOptions: { maxHeight: "80%" } },
106
+ { overlay: true, overlayOptions: modalOverlayOptions() },
101
107
  );
102
108
 
103
109
  return {