@xynogen/pix-pretty 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-pretty",
3
- "version": "1.3.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",
@@ -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,36 +201,10 @@ function tabs(s: string): string {
201
201
  }
202
202
 
203
203
  function termW(): number {
204
- // Delegate to utils.termW which has tty ioctl fallback + resize invalidation
205
- const stderrCols = (process.stderr as { columns?: number }).columns;
206
- const raw =
207
- process.stdout.columns ||
208
- stderrCols ||
209
- Number.parseInt(process.env.COLUMNS ?? "", 10) ||
210
- _readTtyColsDR() ||
211
- DEFAULT_TERM_WIDTH;
212
- return Math.max(80, Math.min(raw, MAX_TERM_WIDTH));
213
- }
214
-
215
- function _readTtyColsDR(): number | undefined {
216
- try {
217
- const { getWindowSize } = require("node:tty") as {
218
- getWindowSize?: (fd: number) => [number, number];
219
- };
220
- if (getWindowSize) {
221
- for (const fd of [1, 2, 0]) {
222
- try {
223
- const [cols] = getWindowSize(fd);
224
- if (cols && cols > 0) return cols;
225
- } catch {
226
- /* not a tty */
227
- }
228
- }
229
- }
230
- } catch {
231
- /* tty unavailable */
232
- }
233
- return undefined;
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));
234
208
  }
235
209
 
236
210
  /** Pad/truncate `s` to exactly `w` visible chars. ANSI-aware. */
@@ -0,0 +1,86 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { visibleWidth } from "@earendil-works/pi-tui";
3
+ import { registerBashTool } from "./bash";
4
+
5
+ class MockTextComponent {
6
+ private text: string;
7
+
8
+ constructor(text = "") {
9
+ this.text = text;
10
+ }
11
+
12
+ setText(value: string): void {
13
+ this.text = value;
14
+ }
15
+
16
+ getText(): string {
17
+ return this.text;
18
+ }
19
+ }
20
+
21
+ describe("registerBashTool", () => {
22
+ it("clamps renderCall to small terminal widths", () => {
23
+ const registered: { renderCall?: (...args: any[]) => MockTextComponent } =
24
+ {};
25
+ const origColumns = process.env.COLUMNS;
26
+ process.env.COLUMNS = "24";
27
+ process.stdout.emit("resize");
28
+ process.stdin.emit("resize");
29
+
30
+ try {
31
+ registerBashTool(
32
+ {
33
+ registerTool(tool: unknown) {
34
+ Object.assign(registered, tool);
35
+ },
36
+ } as any,
37
+ () => ({
38
+ execute: async () => ({
39
+ content: [{ type: "text", text: "ok" }],
40
+ details: undefined,
41
+ }),
42
+ }),
43
+ {
44
+ cwd: process.cwd(),
45
+ sp: (p: string) => p,
46
+ TextComponent: MockTextComponent as any,
47
+ fffState: {} as any,
48
+ cursorStore: {} as any,
49
+ multiGrepRipgrepFallback: async () => ({
50
+ text: "",
51
+ matchCount: 0,
52
+ limitReached: false,
53
+ }),
54
+ },
55
+ );
56
+
57
+ const text = registered.renderCall?.(
58
+ {
59
+ command: 'printf "very very very long line"\necho second\necho third',
60
+ timeout: 30,
61
+ },
62
+ {
63
+ fg: (_key: string, value: string) => value,
64
+ bold: (value: string) => value,
65
+ } as any,
66
+ {
67
+ expanded: false,
68
+ isError: false,
69
+ invalidate: () => {},
70
+ state: {},
71
+ } as any,
72
+ );
73
+
74
+ expect(text).toBeDefined();
75
+ const rendered = text?.getText() ?? "";
76
+ for (const line of rendered.split("\n")) {
77
+ expect(visibleWidth(line)).toBeLessThanOrEqual(24);
78
+ }
79
+ } finally {
80
+ if (origColumns === undefined) delete process.env.COLUMNS;
81
+ else process.env.COLUMNS = origColumns;
82
+ process.stdout.emit("resize");
83
+ process.stdin.emit("resize");
84
+ }
85
+ });
86
+ });
package/src/tools/bash.ts CHANGED
@@ -4,6 +4,7 @@ import type {
4
4
  ExtensionContext,
5
5
  } from "@earendil-works/pi-coding-agent";
6
6
 
7
+ import { truncateToWidth } from "@earendil-works/pi-tui";
7
8
  import { FG_DIM, RST } from "../ansi.js";
8
9
  import { MAX_PREVIEW_LINES } from "../config.js";
9
10
  import { renderBashOutput } from "../renderers.js";
@@ -19,6 +20,7 @@ import {
19
20
  fillToolBackground,
20
21
  getTextContent,
21
22
  isTextContent,
23
+ normalizeLineEndings,
22
24
  renderToolError,
23
25
  rule,
24
26
  setResultDetails,
@@ -37,6 +39,9 @@ export function registerBashTool(
37
39
  pi.registerTool({
38
40
  ...origBash,
39
41
  name: "bash",
42
+ // Full-width framing (rules + bg fill) baked at termW(); the default
43
+ // Box shell pads x by 1 and re-wraps at width-2, splitting every line.
44
+ renderShell: "self",
40
45
 
41
46
  async execute(
42
47
  tid: string,
@@ -84,17 +89,28 @@ export function registerBashTool(
84
89
  renderCtx: RenderContextLike,
85
90
  ) {
86
91
  const cmd = args.command ?? "";
92
+ const displayCmdRaw = cmd.trim();
87
93
  const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
94
+ const label = theme.fg("toolTitle", theme.bold("bash"));
88
95
  const timeout = args.timeout
89
96
  ? ` ${theme.fg("muted", `(${args.timeout}s timeout)`)}`
90
97
  : "";
91
- const displayCmd =
92
- renderCtx.expanded || cmd.length <= 80 ? cmd : `${cmd.slice(0, 77)}…`;
93
- text.setText(
94
- fillToolBackground(
95
- `${theme.fg("toolTitle", theme.bold("bash"))} ${theme.fg("accent", displayCmd)}${timeout}`,
96
- ),
98
+ const cmdLines = displayCmdRaw.split("\n");
99
+ const firstLine = cmdLines[0] ?? "";
100
+ const compactCmd =
101
+ cmdLines.length > 1
102
+ ? `${firstLine} ${theme.fg("muted", `… (+${cmdLines.length - 1} lines)`)}`
103
+ : firstLine;
104
+ const baseCmd = renderCtx.expanded ? displayCmdRaw : compactCmd;
105
+ const availableWidth = Math.max(1, termW() - 1);
106
+ const prefix = `${label} `;
107
+ const reserve = Math.max(0, availableWidth - timeout.length);
108
+ const displayCmd = truncateToWidth(
109
+ theme.fg("accent", baseCmd),
110
+ Math.max(1, reserve - prefix.length),
111
+ "…",
97
112
  );
113
+ text.setText(fillToolBackground(`${prefix}${displayCmd}${timeout}`));
98
114
  return text;
99
115
  },
100
116
 
@@ -113,17 +129,20 @@ export function registerBashTool(
113
129
 
114
130
  const d = result.details as Record<string, unknown> | undefined;
115
131
  if (d?._type === "bashResult") {
132
+ const normalizedText = normalizeLineEndings(d.text as string)
133
+ .replace(/\n{3,}/g, "\n\n")
134
+ .replace(/^\n+|\n+$/g, "");
116
135
  const { summary } = renderBashOutput(
117
- d.text as string,
136
+ normalizedText,
118
137
  d.exitCode as number | null,
119
138
  );
120
- const lines = (d.text as string).split("\n");
139
+ const lines = normalizedText ? normalizedText.split("\n") : [];
121
140
  const lineCount = lines.length;
122
141
  const lineInfo =
123
142
  lineCount > 1 ? ` ${FG_DIM}(${lineCount} lines)${RST}` : "";
124
143
  const header = ` ${summary}${lineInfo}`;
125
144
 
126
- if ((d.text as string).trim()) {
145
+ if (normalizedText) {
127
146
  const maxShow = renderCtx.expanded ? lineCount : MAX_PREVIEW_LINES;
128
147
  const show = lines.slice(0, maxShow);
129
148
  const tw = termW();
package/src/tools/find.ts CHANGED
@@ -37,6 +37,7 @@ export function registerFindTool(
37
37
  pi.registerTool({
38
38
  ...origFind,
39
39
  name: "find",
40
+ renderShell: "self",
40
41
 
41
42
  async execute(
42
43
  tid: string,
package/src/tools/grep.ts CHANGED
@@ -42,6 +42,7 @@ export function registerGrepTool(
42
42
  pi.registerTool({
43
43
  ...origGrep,
44
44
  name: "grep",
45
+ renderShell: "self",
45
46
 
46
47
  async execute(
47
48
  tid: string,
package/src/tools/ls.ts CHANGED
@@ -34,6 +34,7 @@ export function registerLsTool(
34
34
  pi.registerTool({
35
35
  ...origLs,
36
36
  name: "ls",
37
+ renderShell: "self",
37
38
 
38
39
  async execute(
39
40
  tid: string,
@@ -51,6 +51,7 @@ export function registerMultiGrepTool(
51
51
  pi.registerTool({
52
52
  name: "multi_grep",
53
53
  label: "multi_grep",
54
+ renderShell: "self",
54
55
  description: [
55
56
  "Search file contents for lines matching ANY of multiple patterns (OR logic).",
56
57
  "Uses SIMD-accelerated Aho-Corasick multi-pattern matching when FFF is available.",
package/src/tools/read.ts CHANGED
@@ -39,6 +39,9 @@ export function registerReadTool(
39
39
  pi.registerTool({
40
40
  ...origRead,
41
41
  name: "read",
42
+ // Full-width framing baked at termW(); default Box shell pads x by 1
43
+ // and re-wraps at width-2, splitting every line into a padding row.
44
+ renderShell: "self",
42
45
 
43
46
  async execute(
44
47
  tid: string,
@@ -47,6 +47,7 @@ export function registerWriteTool(
47
47
  pi.registerTool({
48
48
  ...origWrite,
49
49
  name: "write",
50
+ renderShell: "self",
50
51
 
51
52
  async execute(
52
53
  tid: string,
package/src/utils.ts CHANGED
@@ -49,11 +49,26 @@ export function fillToolBackground(text: string, bg = BG_BASE): string {
49
49
  }
50
50
 
51
51
  let _cachedTermW: number | undefined;
52
+ let _termWResizeBound = false;
53
+
54
+ function _bindTermWResize(): void {
55
+ if (_termWResizeBound) return;
56
+ _termWResizeBound = true;
57
+ // Persistent listeners: every SIGWINCH invalidates the cache so the next
58
+ // termW() re-reads. `.once` only caught the first resize, leaving width
59
+ // stale on subsequent resizes.
60
+ const invalidate = () => {
61
+ _cachedTermW = undefined;
62
+ };
63
+ process.stdout.on("resize", invalidate);
64
+ process.stdin.on("resize", invalidate);
65
+ }
52
66
 
53
67
  /** Read terminal width — checks all available sources in priority order.
54
68
  * Falls back to querying the controlling tty via fd 1/2/stdin ioctl.
55
69
  * Result is cached and invalidated on SIGWINCH / stdout resize. */
56
70
  export function termW(): number {
71
+ _bindTermWResize();
57
72
  if (_cachedTermW !== undefined) return _cachedTermW;
58
73
 
59
74
  const stderrWithColumns = process.stderr as NodeJS.WriteStream & {
@@ -67,14 +82,6 @@ export function termW(): number {
67
82
  120;
68
83
  _cachedTermW = Math.max(1, Math.min(raw, 210));
69
84
 
70
- // Invalidate on resize so next call re-reads
71
- process.stdout.once("resize", () => {
72
- _cachedTermW = undefined;
73
- });
74
- process.stdin.once("resize", () => {
75
- _cachedTermW = undefined;
76
- });
77
-
78
85
  return _cachedTermW;
79
86
  }
80
87