@xynogen/pix-pretty 1.18.2 → 1.18.4

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.18.2",
3
+ "version": "1.18.4",
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/confirm.ts CHANGED
@@ -97,7 +97,7 @@ export function confirmOverlay(ui: ConfirmUI, opts: ConfirmOptions): Promise<boo
97
97
  const updateCountdown = () => {
98
98
  const remaining = Math.max(0, Math.ceil((deadlineMs - Date.now()) / SECOND_MS));
99
99
  countdownLine =
100
- theme.fg("dim", "Auto-cancel in ") +
100
+ theme.fg("muted", "Auto-cancel in ") +
101
101
  theme.fg(remaining <= COUNTDOWN_WARN_S ? accent : "muted", `${remaining}s`);
102
102
  };
103
103
  updateCountdown();
@@ -121,12 +121,12 @@ export function confirmOverlay(ui: ConfirmUI, opts: ConfirmOptions): Promise<boo
121
121
  render: (w: number) => {
122
122
  const mw = modalWidth(w);
123
123
  const inner = mw - 4;
124
- const footer = [theme.fg("dim", "─".repeat(inner))];
124
+ const footer = [theme.fg("muted", "─".repeat(inner))];
125
125
  if (countdownLine !== undefined) footer.push(countdownLine);
126
126
  footer.push(...selectList.render(inner));
127
127
  footer.push("");
128
128
  footer.push(
129
- theme.fg("dim", "↑↓ choose • ←→/PgUp/PgDn inspect • enter select • esc cancel"),
129
+ theme.fg("muted", "↑↓ choose • ←→/PgUp/PgDn inspect • enter select • esc cancel"),
130
130
  );
131
131
 
132
132
  const result = frameModal({
@@ -141,7 +141,7 @@ export function confirmOverlay(ui: ConfirmUI, opts: ConfirmOptions): Promise<boo
141
141
  bg: (s) => theme.bg("customMessageBg", s),
142
142
  fg: (s) => theme.fg("text", s),
143
143
  overflowLine: ({ page, totalPages }) =>
144
- theme.fg("dim", `←→/PgUp/PgDn inspect • ${page}/${totalPages}`),
144
+ theme.fg("muted", `←→/PgUp/PgDn inspect • ${page}/${totalPages}`),
145
145
  });
146
146
  pager.sync(result);
147
147
  return result.lines;
@@ -158,6 +158,7 @@ describe("showOverlay — confirm mode", () => {
158
158
  title: "SSH FILE TRANSFER",
159
159
  body: [
160
160
  "Intent: Copy a release artifact",
161
+ "Command: scp app.tar.gz host:/tmp",
161
162
  "Host: deploy@example.com",
162
163
  "Direction: Download",
163
164
  "From: /srv/releases/app.tar.gz",
@@ -170,6 +171,8 @@ describe("showOverlay — confirm mode", () => {
170
171
  },
171
172
  );
172
173
  const joined = captured.join("\n");
174
+ expect(joined).toContain("<dim>Intent:</dim> <text>Copy a release artifact</text>");
175
+ expect(joined).toContain("<dim>Command:</dim> <dim>scp app.tar.gz host:/tmp</dim>");
173
176
  expect(joined).toContain("<dim>Host:</dim> <accent>deploy@example.com</accent>");
174
177
  expect(joined).toContain("<dim>Direction:</dim> <warning>Download</warning>");
175
178
  expect(joined).toContain("<dim>From:</dim> <text>/srv/releases/app.tar.gz</text>");
@@ -178,6 +181,44 @@ describe("showOverlay — confirm mode", () => {
178
181
  expect(joined).toContain("<dim>Auth:</dim> <success>SSH key (no password)</success>");
179
182
  });
180
183
 
184
+ // Regression guard (readability fix, pix-pretty 1.18.4): Intent:/Command:
185
+ // lines must go through the label/value colour map — a <dim> label plus a
186
+ // semantic value — never a bare bright value with the label stripped off.
187
+ // Pre-1.18.4 they were sliced and dumped as bright <text> with no label,
188
+ // which was hard to read on the modal background. Command value must be <dim>.
189
+ test("Intent/Command body lines keep a dim label and never drop it", async () => {
190
+ const coloredTheme = {
191
+ fg: (color: string, text: string) => `<${color}>${text}</${color}>`,
192
+ bg: (_color: string, text: string) => text,
193
+ bold: (text: string) => text,
194
+ };
195
+ let captured: string[] = [];
196
+ await showOverlay(
197
+ makeUI(
198
+ (comp) => {
199
+ captured = comp.render(100);
200
+ comp.handleInput(ENTER);
201
+ },
202
+ undefined,
203
+ coloredTheme,
204
+ ),
205
+ {
206
+ mode: "sudo",
207
+ title: "ROOT COMMAND REQUEST",
208
+ accent: "error",
209
+ body: ["Intent: install a package", "Command: apt install foo"],
210
+ timeoutMs: 0,
211
+ },
212
+ );
213
+ const joined = captured.join("\n");
214
+ // Command label + value both dim; intent value stays readable text but is
215
+ // always prefixed by a dim label (never a bare label-less value).
216
+ expect(joined).toContain("<dim>Command:</dim> <dim>apt install foo</dim>");
217
+ expect(joined).toContain("<dim>Intent:</dim> <text>install a package</text>");
218
+ // The pre-1.18.4 bug: label stripped, value dumped bright with no dim label.
219
+ expect(joined).not.toContain("<text>apt install foo</text>"); // command value is dim, not text
220
+ });
221
+
181
222
  test("wraps a long body command instead of truncating it", async () => {
182
223
  // A command far wider than any modal width — must survive in full, wrapped.
183
224
  const longCmd = `echo ${"pix-gate-installed-or-linked ".repeat(8)}done`;
@@ -176,16 +176,16 @@ function buildSections(opts: {
176
176
  const inner = width - 4; // CHROME = 2 border + 2 padding
177
177
  const header = [theme.fg(accent, theme.bold(config.title))];
178
178
  const body = (config.body ?? []).map((line) => {
179
- if (line.startsWith("Intent:")) return theme.fg("text", line.slice(7).trimStart());
180
- if (line.startsWith("Command:")) return theme.fg("dim", line.slice(8).trimStart());
181
179
  if (line.startsWith("Warning:")) return theme.fg("warning", line);
182
- if (line.startsWith("(") && line.endsWith(")")) return theme.fg("dim", line);
180
+ if (line.startsWith("(") && line.endsWith(")")) return theme.fg("muted", line);
183
181
 
184
182
  const separator = line.indexOf(":");
185
183
  if (separator < 1) return theme.fg("text", line);
186
184
  const label = line.slice(0, separator + 1);
187
185
  const value = line.slice(separator + 1).trimStart();
188
186
  const valueColors: Record<string, string> = {
187
+ "Intent:": "text",
188
+ "Command:": "dim",
189
189
  "Host:": "accent",
190
190
  "Direction:": "warning",
191
191
  "From:": "text",
@@ -198,7 +198,7 @@ function buildSections(opts: {
198
198
  ? `${theme.fg("dim", label)} ${theme.fg(valueColor, value)}`
199
199
  : theme.fg("text", line);
200
200
  });
201
- const footer = [theme.fg("dim", "─".repeat(inner))];
201
+ const footer = [theme.fg("muted", "─".repeat(inner))];
202
202
 
203
203
  if (countdownLine !== undefined) footer.push(countdownLine);
204
204
 
@@ -206,14 +206,14 @@ function buildSections(opts: {
206
206
  if (stage === "select") {
207
207
  footer.push(...selectList.render(inner));
208
208
  footer.push("");
209
- footer.push(theme.fg("dim", "↑↓ choose • ←→/PgUp/PgDn inspect • enter select • esc deny"));
209
+ footer.push(theme.fg("muted", "↑↓ choose • ←→/PgUp/PgDn inspect • enter select • esc deny"));
210
210
  } else {
211
211
  const label = config.mode === "sudo" ? (config.passwordLabel ?? "Sudo password:") : "Password:";
212
- footer.push(theme.fg("muted", label));
212
+ footer.push(theme.fg("dim", label));
213
213
  if (passwordStatus) footer.push(theme.fg("error", passwordStatus));
214
214
  footer.push(...maskedInput.render(inner));
215
215
  footer.push("");
216
- footer.push(theme.fg("dim", "←→/PgUp/PgDn inspect • enter confirm • esc cancel"));
216
+ footer.push(theme.fg("muted", "←→/PgUp/PgDn inspect • enter confirm • esc cancel"));
217
217
  }
218
218
 
219
219
  return { header, body, footer };
@@ -302,7 +302,7 @@ export function showOverlay(ui: OverlayUI, config: OverlayConfig): Promise<Overl
302
302
  // Arm the dead-man's switch (only when a timeout was requested).
303
303
  if (timeoutMs > 0) {
304
304
  const urgencyColor = (s: number): string =>
305
- s <= 5 ? "error" : s <= 15 ? "warning" : "dim";
305
+ s <= 5 ? "error" : s <= 15 ? "warning" : "muted";
306
306
  const countdownText = (s: number): string => {
307
307
  const color = urgencyColor(s);
308
308
  const text = `auto-deny in ${s}s`;
@@ -389,7 +389,7 @@ export function showOverlay(ui: OverlayUI, config: OverlayConfig): Promise<Overl
389
389
  bg: (s) => theme.bg("customMessageBg", s),
390
390
  fg: (s) => theme.fg("text", s),
391
391
  overflowLine: ({ page, totalPages }) =>
392
- theme.fg("dim", `←→/PgUp/PgDn inspect • ${page}/${totalPages}`),
392
+ theme.fg("muted", `←→/PgUp/PgDn inspect • ${page}/${totalPages}`),
393
393
  });
394
394
  pager.sync(result);
395
395
  return result.lines;
@@ -546,14 +546,14 @@ interface FgTheme {
546
546
 
547
547
  /**
548
548
  * Canonical SelectList theme for interactive overlays.
549
- * accent = active/selected, muted = descriptions, dim = scroll/hints, warning = no-match.
549
+ * accent = active/selected, dim = descriptions, muted = scroll/hints, warning = no-match.
550
550
  */
551
551
  export function selectListTheme(theme: FgTheme, accent = "accent"): SelectListThemeConfig {
552
552
  return {
553
553
  selectedPrefix: (t) => theme.fg(accent, t),
554
554
  selectedText: (t) => theme.fg(accent, t),
555
- description: (t) => theme.fg("muted", t),
556
- scrollInfo: (t) => theme.fg("dim", t),
555
+ description: (t) => theme.fg("dim", t),
556
+ scrollInfo: (t) => theme.fg("muted", t),
557
557
  noMatch: (t) => theme.fg("warning", t),
558
558
  };
559
559
  }
package/src/progress.ts CHANGED
@@ -92,7 +92,7 @@ export function openProgress(ui: ProgressUI, title: string, accent = "accent"):
92
92
  maxHeight: terminalModalHeight(tui.terminal?.rows),
93
93
  minHeight: MIN_MODAL_HEIGHT,
94
94
  header: [theme.fg(accent, theme.bold(title))],
95
- body: [`${theme.fg(accent, SPINNER[frame] ?? "")} ${theme.fg("muted", labelValue)}`],
95
+ body: [`${theme.fg(accent, SPINNER[frame] ?? "")} ${theme.fg("dim", labelValue)}`],
96
96
  color: (s) => theme.fg(accent, s),
97
97
  bg: (s) => theme.bg("customMessageBg", s),
98
98
  }).lines;
package/src/utils.test.ts CHANGED
@@ -300,6 +300,16 @@ describe("formatJson", () => {
300
300
  describe("collapsed tool rows", () => {
301
301
  const rowTheme = { fg: (_key: string, text: string) => text, bold: (text: string) => text };
302
302
 
303
+ it("uses dim for the target and muted for tertiary metadata", () => {
304
+ const taggedTheme = {
305
+ fg: (key: string, text: string) => `<${key}>${text}</${key}>`,
306
+ bold: (text: string) => text,
307
+ };
308
+ expect(formatCollapsedToolRow(taggedTheme, "read", "src/a.ts", "12 lines")).toContain(
309
+ "<dim>src/a.ts</dim> <muted>·</muted> <muted>12 lines</muted>",
310
+ );
311
+ });
312
+
303
313
  it("renders a consistent status, tool, target, and metadata row", () => {
304
314
  // The status marker is width-normalized to 2 cells (padIcon) so wide glyphs
305
315
  // align with narrow ones; a 1-cell `✓` therefore carries one pad space.
package/src/utils.ts CHANGED
@@ -293,8 +293,8 @@ export function formatCollapsedToolRow(
293
293
  const icon = padIcon(COLLAPSED_TOOL_GLYPH[status]);
294
294
  const parts = [
295
295
  `${theme.fg(status, icon)} ${theme.fg("toolTitle", theme.bold(tool))}`,
296
- target ? theme.fg("muted", target) : "",
297
- meta ? `${theme.fg("dim", "·")} ${theme.fg("dim", meta)}` : "",
296
+ target ? theme.fg("dim", target) : "",
297
+ meta ? `${theme.fg("muted", "·")} ${theme.fg("muted", meta)}` : "",
298
298
  ].filter(Boolean);
299
299
  return parts.join(" ");
300
300
  }
@@ -406,7 +406,7 @@ export function renderDimPreview(
406
406
  const header = opts.header ? ` ${theme.fg("dim", opts.header)}` : undefined;
407
407
  const overflow =
408
408
  lines.length > maxLines
409
- ? ` ${theme.fg("dim", `… ${pluralize(lines.length - maxLines, "more line")}`)}`
409
+ ? ` ${theme.fg("muted", `… ${pluralize(lines.length - maxLines, "more line")}`)}`
410
410
  : undefined;
411
411
 
412
412
  if (opts.frame) {