@heyhuynhgiabuu/pi-pretty 0.4.3 → 0.5.0

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": "@heyhuynhgiabuu/pi-pretty",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "Pretty terminal output for pi — syntax-highlighted file reads, colored bash output, tree-view directory listings, and more.",
5
5
  "author": "huynhgiabuu",
6
6
  "license": "MIT",
@@ -26,10 +26,14 @@
26
26
  "@shikijs/cli": "^4.0.2"
27
27
  },
28
28
  "peerDependencies": {
29
+ "@earendil-works/pi-coding-agent": "*",
30
+ "@earendil-works/pi-tui": "*",
29
31
  "@mariozechner/pi-coding-agent": "*",
30
32
  "@mariozechner/pi-tui": "*"
31
33
  },
32
34
  "devDependencies": {
35
+ "@earendil-works/pi-coding-agent": "*",
36
+ "@earendil-works/pi-tui": "*",
33
37
  "@types/node": "^20.0.0",
34
38
  "typescript": "^5.0.0",
35
39
  "@biomejs/biome": "^2.3.5",
@@ -0,0 +1,28 @@
1
+ # pi-pretty 0.4.4
2
+
3
+ ## Summary
4
+ This patch release fixes terminal background tearing/misaligned cells by enforcing the pi-tui line-width contract for background-filled tool output.
5
+
6
+ ## What changed
7
+ - Clamp every background-filled tool line to the active terminal width before adding background padding.
8
+ - Use `@mariozechner/pi-tui` `truncateToWidth()` and `visibleWidth()` so ANSI escapes and wide Unicode cells are measured correctly.
9
+ - Preserve the configured tool background after ANSI reset sequences even when truncation occurs.
10
+ - Remove the 80-column minimum width floor so narrow terminals do not receive over-wide rendered lines.
11
+ - Replace custom read-output truncation with pi-tui's ANSI-aware truncation helper.
12
+ - Add regression coverage for expanded ANSI tool headers, wide characters, and narrow terminal widths.
13
+
14
+ ## Files
15
+ - `src/index.ts`
16
+ - `test/bash-rendering.test.ts`
17
+ - `package.json`
18
+ - `package-lock.json`
19
+
20
+ ## Verification
21
+ - `npm run lint` ✅
22
+ - `npm run typecheck` ✅
23
+ - `npm test` ✅ (69 tests)
24
+
25
+ ## Upgrade notes
26
+ No configuration changes are required.
27
+
28
+ This release specifically follows the pi-tui requirement that rendered component lines must not exceed the available width, preventing terminal auto-wrap artifacts in red/error and themed tool backgrounds.
package/src/index.ts CHANGED
@@ -28,7 +28,7 @@ import { mkdirSync, readFileSync } from "node:fs";
28
28
  import { basename, dirname, extname, join, relative } from "node:path";
29
29
 
30
30
  import type { FileFinder, FileItem, GrepResult, SearchResult } from "@ff-labs/fff-node";
31
- import type { ImageContent, TextContent } from "@mariozechner/pi-ai";
31
+ import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
32
32
  import type {
33
33
  AgentToolResult,
34
34
  AgentToolUpdateCallback,
@@ -40,7 +40,8 @@ import type {
40
40
  LsToolInput,
41
41
  ReadToolInput,
42
42
  ToolRenderResultOptions,
43
- } from "@mariozechner/pi-coding-agent";
43
+ } from "@earendil-works/pi-coding-agent";
44
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
44
45
  import { codeToANSI } from "@shikijs/cli";
45
46
  import type { BundledLanguage, BundledTheme } from "shiki";
46
47
 
@@ -148,7 +149,6 @@ function renderToolError(error: string, theme: FgTheme): string {
148
149
  }
149
150
 
150
151
  const ESC_RE = "\u001b";
151
- const ANSI_RE = new RegExp(`${ESC_RE}\\[[0-9;]*m`, "g");
152
152
  const ANSI_CAPTURE_RE = new RegExp(`${ESC_RE}\\[([0-9;]*)m`, "g");
153
153
 
154
154
  // ---------------------------------------------------------------------------
@@ -174,10 +174,6 @@ function normalizeShikiContrast(ansi: string): string {
174
174
  // Utilities
175
175
  // ---------------------------------------------------------------------------
176
176
 
177
- function strip(s: string): string {
178
- return s.replace(ANSI_RE, "");
179
- }
180
-
181
177
  function normalizeLineEndings(text: string): string {
182
178
  return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
183
179
  }
@@ -195,8 +191,9 @@ function fillToolBackground(text: string, bg = BG_BASE): string {
195
191
  .split("\n")
196
192
  .map((line) => {
197
193
  const normalized = preserveToolBackground(line, bg);
198
- const padding = Math.max(0, width - strip(normalized).length);
199
- return `${bg}${normalized}${" ".repeat(padding)}${RST}`;
194
+ const fitted = preserveToolBackground(truncateToWidth(normalized, width, ""), bg);
195
+ const padding = Math.max(0, width - visibleWidth(fitted));
196
+ return `${bg}${fitted}${" ".repeat(padding)}${RST}`;
200
197
  })
201
198
  .join("\n");
202
199
  }
@@ -205,7 +202,7 @@ function termW(): number {
205
202
  const stderrWithColumns = process.stderr as NodeJS.WriteStream & { columns?: number };
206
203
  const raw =
207
204
  process.stdout.columns || stderrWithColumns.columns || Number.parseInt(process.env.COLUMNS ?? "", 10) || 200;
208
- return Math.max(80, Math.min(raw - 4, 210));
205
+ return Math.max(1, Math.min(raw - 4, 210));
209
206
  }
210
207
 
211
208
  function shortPath(cwd: string, home: string, p: string): string {
@@ -621,7 +618,7 @@ async function renderFileContent(
621
618
  const endLine = startLine + show.length - 1;
622
619
  const nw = Math.max(3, String(endLine).length);
623
620
  const gw = nw + 3; // num + " │ "
624
- const cw = Math.max(20, tw - gw);
621
+ const cw = Math.max(1, tw - gw);
625
622
 
626
623
  const out: string[] = [];
627
624
  out.push(rule(tw));
@@ -629,25 +626,7 @@ async function renderFileContent(
629
626
  for (let i = 0; i < hl.length; i++) {
630
627
  const ln = startLine + i;
631
628
  const code = hl[i] ?? show[i] ?? "";
632
- const plain = strip(code);
633
- // Truncate if wider than available
634
- let display = code;
635
- if (plain.length > cw) {
636
- let vis = 0;
637
- let j = 0;
638
- while (j < code.length && vis < cw - 1) {
639
- if (code[j] === "\x1b") {
640
- const e = code.indexOf("m", j);
641
- if (e !== -1) {
642
- j = e + 1;
643
- continue;
644
- }
645
- }
646
- vis++;
647
- j++;
648
- }
649
- display = `${code.slice(0, j)}${RST}${FG_DIM}›${RST}`;
650
- }
629
+ const display = truncateToWidth(code, cw, `${FG_DIM}›`);
651
630
  out.push(`${lnum(ln, nw)} ${FG_RULE}│${RST} ${display}${RST}`);
652
631
  }
653
632
 
@@ -1038,13 +1017,13 @@ export default function piPrettyExtension(pi: PiPrettyApi, deps?: PiPrettyDeps):
1038
1017
  _fffDbDir = null;
1039
1018
  } else {
1040
1019
  try {
1041
- sdk = require("@mariozechner/pi-coding-agent");
1020
+ sdk = require("@earendil-works/pi-coding-agent");
1042
1021
  createReadTool = sdk.createReadToolDefinition ?? sdk.createReadTool;
1043
1022
  createBashTool = sdk.createBashToolDefinition ?? sdk.createBashTool;
1044
1023
  createLsTool = sdk.createLsToolDefinition ?? sdk.createLsTool;
1045
1024
  createFindTool = sdk.createFindToolDefinition ?? sdk.createFindTool;
1046
1025
  createGrepTool = sdk.createGrepToolDefinition ?? sdk.createGrepTool;
1047
- TextComponent = require("@mariozechner/pi-tui").Text;
1026
+ TextComponent = require("@earendil-works/pi-tui").Text;
1048
1027
  } catch {
1049
1028
  return;
1050
1029
  }
@@ -1,3 +1,4 @@
1
+ import { visibleWidth } from "@earendil-works/pi-tui";
1
2
  import { describe, expect, it } from "vitest";
2
3
 
3
4
  import piPrettyExtension from "../src/index.js";
@@ -18,6 +19,11 @@ const mockTheme = {
18
19
  bold: (text: string) => text,
19
20
  };
20
21
 
22
+ const ansiMockTheme = {
23
+ fg: (_key: string, text: string) => `\x1b[31m${text}\x1b[0m`,
24
+ bold: (text: string) => `\x1b[1m${text}\x1b[22m`,
25
+ };
26
+
21
27
  function mockToolFactory(exec: any) {
22
28
  return (_cwd: string) => ({
23
29
  name: "mock",
@@ -27,6 +33,20 @@ function mockToolFactory(exec: any) {
27
33
  });
28
34
  }
29
35
 
36
+ function withStdoutColumns<T>(columns: number, fn: () => T): T {
37
+ const descriptor = Object.getOwnPropertyDescriptor(process.stdout, "columns");
38
+ Object.defineProperty(process.stdout, "columns", { configurable: true, value: columns });
39
+ try {
40
+ return fn();
41
+ } finally {
42
+ if (descriptor) {
43
+ Object.defineProperty(process.stdout, "columns", descriptor);
44
+ } else {
45
+ delete (process.stdout as NodeJS.WriteStream & { columns?: number }).columns;
46
+ }
47
+ }
48
+ }
49
+
30
50
  function loadBashTool() {
31
51
  const noopExec = async () => ({ content: [{ type: "text", text: "" }] });
32
52
  const tools = new Map<string, any>();
@@ -106,4 +126,42 @@ describe("bash renderCall expansion", () => {
106
126
  expect(collapsed.getText()).toContain("5s timeout");
107
127
  expect(expanded.getText()).toContain("5s timeout");
108
128
  });
129
+
130
+ it("truncates expanded ANSI tool headers to the terminal width before padding backgrounds", () => {
131
+ withStdoutColumns(84, () => {
132
+ const bashTool = loadBashTool();
133
+ const command = `printf '${"界".repeat(120)}'`;
134
+
135
+ const rendered = bashTool.renderCall({ command }, ansiMockTheme, {
136
+ lastComponent: new MockText(),
137
+ isError: false,
138
+ state: {},
139
+ expanded: true,
140
+ invalidate: () => {},
141
+ });
142
+
143
+ for (const line of rendered.getText().split("\n")) {
144
+ expect(visibleWidth(line)).toBeLessThanOrEqual(80);
145
+ }
146
+ });
147
+ });
148
+
149
+ it("does not exceed narrow terminal widths", () => {
150
+ withStdoutColumns(24, () => {
151
+ const bashTool = loadBashTool();
152
+ const command = `printf '${"x".repeat(120)}'`;
153
+
154
+ const rendered = bashTool.renderCall({ command }, ansiMockTheme, {
155
+ lastComponent: new MockText(),
156
+ isError: false,
157
+ state: {},
158
+ expanded: true,
159
+ invalidate: () => {},
160
+ });
161
+
162
+ for (const line of rendered.getText().split("\n")) {
163
+ expect(visibleWidth(line)).toBeLessThanOrEqual(20);
164
+ }
165
+ });
166
+ });
109
167
  });