pi-tui-status-beautifier 1.1.0 β†’ 1.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+ ## [1.1.1] - 2026-08-23
3
+ ### Added
4
+ - Memory-hoisted visual cell-width checks of character sequences to correctly align TUI status templates (Finding 5)
5
+ - State cache-based dirty-checking for status updates to prevent redundant redrawing and layout flickering (Finding 6)
6
+ ### Fixed
7
+ - Fixed TUI alignment issues involving full-width CJK characters and emojis in layout paddings
8
+
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jason Song (@3ZEROS12)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -2,10 +2,15 @@
2
2
 
3
3
  A premium status bar beautifier extension for the Pi Terminal TUI. Instantly transforms boring status lines into brand-inspired themes and clean layouts.
4
4
 
5
- ## πŸš€ Installation & Usage (Recommended)
5
+ ## πŸš€ Installation & Usage
6
6
 
7
- Since this package is published on GitHub, you can install it instantly in the Pi TUI command environment using the following single line:
7
+ Install directly via the Pi package manager:
8
8
 
9
+ ```bash
10
+ pi install npm:pi-tui-status-beautifier
11
+ ```
12
+
13
+ Or install from git:
9
14
  ```bash
10
15
  pi install git:github.com/3ZEROS12/pi-tui-status-beautifier
11
16
  ```
@@ -4,13 +4,13 @@ import * as path from "path";
4
4
 
5
5
  // Mapping of internal package names to clean human-readable TUI status names
6
6
  const COMMON_NAMES: Record<string, string> = {
7
- "plannotator": "plan",
7
+ plannotator: "plan",
8
8
  "plannotator-review": "plan",
9
9
  "pi-mcp-adapter": "mcp",
10
10
  "subagent-slash": "subagent",
11
11
  "subagent-slash-text": "subagent",
12
12
  "pi-wechat-assistant": "wechat",
13
- "wechat-assistant": "wechat"
13
+ "wechat-assistant": "wechat",
14
14
  };
15
15
 
16
16
  // Store original statuses to allow dynamic redrawing on configuration changes.
@@ -20,11 +20,16 @@ const originalStatuses = new Map<string, string | undefined>();
20
20
  // Memoization cache for Google colorized text to reduce GC overhead and theme calls.
21
21
  const googleColorizeCache = new Map<string, string>();
22
22
 
23
+ // State Cache for Dirty-Checking
24
+ const lastRenderedStatus = new Map<string, string | undefined>();
25
+
23
26
  // Safe, non-backtracking ReDoS-mitigated ANSI escape sequence matcher (supporting ; and :)
24
- const ANSI_STRIP_REGEX = /[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[-a-zA-Z\d\/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-ntqry=><~]))/g;
27
+ const ANSI_STRIP_REGEX =
28
+ /[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-ntqry=><~]))/g;
25
29
 
26
30
  // Hoisted regex patterns to prevent instantiation and GC overhead in the render hot-path
27
- const STATUS_PATTERN = /[πŸŸ’πŸ”΄πŸŸ‘βšͺβΈβœ…βŒβœ“βœ—?βœ”βœ–β˜‘β˜β—†πŸ“‹]|[0-9]+\/[0-9]+|active|online|offline|running|paused|error|success|connected|disconnected/i;
31
+ const STATUS_PATTERN =
32
+ /[πŸŸ’πŸ”΄πŸŸ‘βšͺβΈβœ…βŒβœ“βœ—?βœ”βœ–β˜‘β˜β—†πŸ“‹]|[0-9]+\/[0-9]+|active|online|offline|running|paused|error|success|connected|disconnected/i;
28
33
  const NAME_SUFFIX_PATTERN = /-(extension|plugin|assistant|adapter|slash-text|slash|text|widget)$/gi;
29
34
  const FRACTION_PATTERN = /(\d+\/\d+|\d+%\s*)/;
30
35
  const PARENTHESES_PATTERN = /\((\d+)\)/;
@@ -38,7 +43,7 @@ const settingsPath = path.join(home, ".pi/agent/settings.json");
38
43
  // Command-Driven I/O State:
39
44
  // Loaded ONCE synchronously during extension loading, thereafter handled entirely in memory.
40
45
  // Disk I/O only occurs when the user triggers the "/beautify" selection command.
41
- let currentStyle = "minimal";
46
+ let currentStyle = "minimal";
42
47
 
43
48
  try {
44
49
  if (fs.existsSync(settingsPath)) {
@@ -55,13 +60,14 @@ try {
55
60
  // Google playful multi-color letter colorizer with type safeguards and memoization
56
61
  export function googleColorize(text: string, theme: any): string {
57
62
  if (!theme || typeof theme.fg !== "function") return text;
58
-
63
+
59
64
  const cached = googleColorizeCache.get(text);
60
65
  if (cached !== undefined) return cached;
61
66
 
62
67
  // Google's brand colors cycle: Blue, Red, Yellow, Green
63
68
  const colors = ["accent", "error", "warning", "success"];
64
- const colorized = text.split("")
69
+ const colorized = text
70
+ .split("")
65
71
  .map((char, index) => {
66
72
  const color = colors[index % colors.length];
67
73
  return theme.fg(color, char);
@@ -73,16 +79,19 @@ export function googleColorize(text: string, theme: any): string {
73
79
  }
74
80
 
75
81
  // Clean status text and map state and details
76
- export function beautifyStatus(key: string, originalVal: string | undefined, theme: any, style: string): string | undefined {
82
+ export function beautifyStatus(
83
+ key: string,
84
+ originalVal: string | undefined,
85
+ theme: any,
86
+ style: string
87
+ ): string | undefined {
77
88
  if (originalVal === undefined) return undefined;
78
89
  if (style === "off") return originalVal; // Pass through untouched if disabled
79
90
 
80
91
  try {
81
92
  // Fast-path: bypass Regex search if no ESC or CSI bytes exist to reduce hot redraw latency
82
93
  const hasAnsi = originalVal.includes("\u001B") || originalVal.includes("\u009B");
83
- const cleanVal = hasAnsi
84
- ? originalVal.replace(ANSI_STRIP_REGEX, "").trim()
85
- : originalVal.trim();
94
+ const cleanVal = hasAnsi ? originalVal.replace(ANSI_STRIP_REGEX, "").trim() : originalVal.trim();
86
95
 
87
96
  if (cleanVal === "") return "";
88
97
 
@@ -100,11 +109,10 @@ export function beautifyStatus(key: string, originalVal: string | undefined, the
100
109
  if (lastSlash !== -1) {
101
110
  name = name.slice(lastSlash + 1);
102
111
  }
103
- name = name
104
- .replace(NAME_SUFFIX_PATTERN, "")
105
- .toLowerCase();
106
- name = name.slice(0, 10);
112
+ name = name.replace(NAME_SUFFIX_PATTERN, "").toLowerCase();
107
113
  }
114
+ // Optimization (Finding 5): Use visual cell width calculations rather than standard .length for CJK/emojis
115
+ const visualName = padToVisualWidth(sliceToVisualWidth(name, 10), 10);
108
116
 
109
117
  // 2. Extract metrics/progress details safely (e.g. (2) or fraction 2/5 or count)
110
118
  let details: string | undefined;
@@ -172,20 +180,20 @@ export function beautifyStatus(key: string, originalVal: string | undefined, the
172
180
  switch (style) {
173
181
  case "apple": {
174
182
  const separator = hasThemeFg ? theme.fg("dim", " β”‚ ") : " β”‚ ";
175
- const dimmedName = hasThemeFg ? theme.fg("dim", name) : name;
183
+ const dimmedName = hasThemeFg ? theme.fg("dim", visualName) : visualName;
176
184
  const detailStr = details ? ` (${details})` : "";
177
185
  return `${dimmedName}${separator}${coloredIndicator}${detailStr}`;
178
186
  }
179
187
  case "openai": {
180
188
  const spirograph = hasThemeFg ? theme.fg("success", "❂") : "❂";
181
189
  const detailStr = details ? ` [${details}]` : "";
182
- const dimmedName = hasThemeFg ? theme.fg("muted", name) : name;
190
+ const dimmedName = hasThemeFg ? theme.fg("muted", visualName) : visualName;
183
191
  return `${spirograph} ${dimmedName}${detailStr}`;
184
192
  }
185
193
  case "anthropic": {
186
194
  const organicStar = "✦";
187
195
  const coloredStar = hasThemeFg ? theme.fg(state, organicStar) : organicStar;
188
- const dimmedName = hasThemeFg ? theme.fg("muted", name) : name;
196
+ const dimmedName = hasThemeFg ? theme.fg("muted", visualName) : visualName;
189
197
  const detailStr = details ? ` ❖ ${details}` : "";
190
198
  return `${dimmedName} ${coloredStar}${detailStr}`;
191
199
  }
@@ -194,39 +202,37 @@ export function beautifyStatus(key: string, originalVal: string | undefined, the
194
202
  const detailStr = details ? ` β”‚ ${details}` : "";
195
203
  const prefix = hasThemeFg ? theme.fg("dim", "[") : "[";
196
204
  const suffix = hasThemeFg ? theme.fg("dim", "]") : "]";
197
- return `${windowsLogo} ${prefix}${name}${suffix} ${coloredIndicator}${detailStr}`;
205
+ return `${windowsLogo} ${prefix}${visualName}${suffix} ${coloredIndicator}${detailStr}`;
198
206
  }
199
207
  case "google": {
200
- const coloredName = googleColorize(name, theme);
208
+ const coloredName = googleColorize(visualName, theme);
201
209
  const detailStr = details ? `:${details}` : "";
202
210
  return `${coloredName} βž” ${coloredIndicator}${detailStr}`;
203
211
  }
204
212
  case "glass": {
205
213
  const prefix = hasThemeFg ? theme.fg("dim", "β–• ") : "β–• ";
206
214
  const suffix = hasThemeFg ? theme.fg("dim", " ▏") : " ▏";
207
- const dimmedName = hasThemeFg ? theme.fg("muted", name) : name;
215
+ const dimmedName = hasThemeFg ? theme.fg("muted", visualName) : visualName;
208
216
  const detailStr = details ? ` (${details})` : "";
209
217
  return `${prefix}${dimmedName} ${coloredIndicator}${detailStr}${suffix}`;
210
218
  }
211
219
  case "glow": {
212
220
  const label = ` ${char}${details ? ` ${details}` : ""} `;
213
221
  const hasInverse = theme && typeof theme.inverse === "function";
214
- const coloredBadge = (hasInverse && hasThemeFg)
215
- ? theme.inverse(theme.fg(state, label))
216
- : `[${label}]`;
217
- const dimmedName = hasThemeFg ? theme.fg("muted", name) : name;
222
+ const coloredBadge = hasInverse && hasThemeFg ? theme.inverse(theme.fg(state, label)) : `[${label}]`;
223
+ const dimmedName = hasThemeFg ? theme.fg("muted", visualName) : visualName;
218
224
  return `${dimmedName} ${coloredBadge}`;
219
225
  }
220
226
  case "matrix": {
221
- const prefix = hasThemeFg ? theme.fg("dim", " β¦— ") : " β¦— ";
222
- const suffix = hasThemeFg ? theme.fg("dim", " ⦘") : " ⦘";
227
+ const prefix = hasThemeFg ? theme.fg("dim", " \u2397 ") : " \u2397 ";
228
+ const suffix = hasThemeFg ? theme.fg("dim", " \u2398") : " \u2398";
223
229
  const separator = hasThemeFg ? theme.fg("dim", " | ") : " | ";
224
230
  const detailStr = details ? `${separator}${details}` : "";
225
- return `${name}${prefix}${coloredIndicator}${detailStr}${suffix}`;
231
+ return `${visualName}${prefix}${coloredIndicator}${detailStr}${suffix}`;
226
232
  }
227
233
  case "minimal": {
228
234
  const separator = hasThemeFg ? theme.fg("dim", " ❯ ") : " ❯ ";
229
- const dimmedName = hasThemeFg ? theme.fg("muted", name) : name;
235
+ const dimmedName = hasThemeFg ? theme.fg("muted", visualName) : visualName;
230
236
  const detailStr = details ? ` (${details})` : "";
231
237
  return `${dimmedName}${separator}${coloredIndicator}${detailStr}`;
232
238
  }
@@ -239,6 +245,91 @@ export function beautifyStatus(key: string, originalVal: string | undefined, the
239
245
  }
240
246
  }
241
247
 
248
+ /**
249
+ * Calculates the visual column cell width of a string (Finding 5), taking into account
250
+ * full-width Unicode characters, East Asian characters, and emojis.
251
+ */
252
+ export function getStringWidth(str: string): number {
253
+ if (!str) return 0;
254
+ // Strip ANSI escape sequences & control codes before counting visual width
255
+ const clean = str.replace(ANSI_STRIP_REGEX, "");
256
+ let width = 0;
257
+ for (let i = 0; i < clean.length; i++) {
258
+ const code = clean.charCodeAt(i);
259
+ if (isFullWidth(code)) {
260
+ width += 2;
261
+ } else {
262
+ width += 1;
263
+ }
264
+ }
265
+ return width;
266
+ }
267
+
268
+ /**
269
+ * Checks if a character unicode code point is full-width (East Asian Width).
270
+ */
271
+ function isFullWidth(code: number): boolean {
272
+ if (isNaN(code)) return false;
273
+ return (
274
+ code >= 0x1100 &&
275
+ (code <= 0x115f ||
276
+ code === 0x2329 ||
277
+ code === 0x232a ||
278
+ // CJK Radicals Supplement .. Enclosed CJK Letters and Months
279
+ (code >= 0x2e80 && code <= 0x3247 && code !== 0x303f) ||
280
+ // Enclosed CJK Letters and Months .. CJK Unified Ideographs Extension A
281
+ (code >= 0x3250 && code <= 0x4dbf) ||
282
+ // CJK Unified Ideographs .. Yi Radicals
283
+ (code >= 0x4e00 && code <= 0xa4c6) ||
284
+ // Hangul Jamo Extended-A
285
+ (code >= 0xa960 && code <= 0xa97c) ||
286
+ // Hangul Syllables
287
+ (code >= 0xac00 && code <= 0xd7a3) ||
288
+ // CJK Compatibility Ideographs
289
+ (code >= 0xf900 && code <= 0xfaff) ||
290
+ // Vertical Forms
291
+ (code >= 0xfe10 && code <= 0xfe19) ||
292
+ // CJK Compatibility Forms .. Small Form Variants
293
+ (code >= 0xfe30 && code <= 0xfe6b) ||
294
+ // Halfwidth and Fullwidth Forms
295
+ (code >= 0xff01 && code <= 0xff60) ||
296
+ (code >= 0xffe0 && code <= 0xffe6) ||
297
+ // East Asian Wide characters beyond BMP
298
+ (code >= 0x1b000 && code <= 0x1b001) ||
299
+ (code >= 0x1f200 && code <= 0x1f251) ||
300
+ (code >= 0x20000 && code <= 0x3fffd))
301
+ );
302
+ }
303
+
304
+ /**
305
+ * Slices a string to a maximum visual cell width.
306
+ */
307
+ export function sliceToVisualWidth(str: string, maxWidth: number): string {
308
+ let width = 0;
309
+ let sliced = "";
310
+ for (let i = 0; i < str.length; i++) {
311
+ const code = str.charCodeAt(i);
312
+ const charWidth = isFullWidth(code) ? 2 : 1;
313
+ if (width + charWidth > maxWidth) {
314
+ break;
315
+ }
316
+ width += charWidth;
317
+ sliced += str.charAt(i);
318
+ }
319
+ return sliced;
320
+ }
321
+
322
+ /**
323
+ * Pads a string's visual cell width to target width with trailing spaces.
324
+ */
325
+ export function padToVisualWidth(str: string, targetWidth: number, padChar = " "): string {
326
+ const currentWidth = getStringWidth(str);
327
+ if (currentWidth >= targetWidth) {
328
+ return str;
329
+ }
330
+ return str + padChar.repeat(targetWidth - currentWidth);
331
+ }
332
+
242
333
  export default function (pi: ExtensionAPI) {
243
334
  pi.on("session_start", async (_event, ctx) => {
244
335
  if (!ctx.hasUI || !ctx.ui) return;
@@ -246,6 +337,7 @@ export default function (pi: ExtensionAPI) {
246
337
  // Flush stale statuses from previous sessions to prevent memory leaks and clear color cache
247
338
  originalStatuses.clear();
248
339
  googleColorizeCache.clear();
340
+ lastRenderedStatus.clear();
249
341
 
250
342
  const originalSetStatus = ctx.ui.setStatus;
251
343
  if (originalSetStatus && !(originalSetStatus as any).__beautifierHooked) {
@@ -255,7 +347,14 @@ export default function (pi: ExtensionAPI) {
255
347
 
256
348
  // Uses in-memory synchronization (zero file I/O overhead on render pipeline ticks)
257
349
  const beautified = beautifyStatus(key, value, ctx.ui.theme, currentStyle);
258
-
350
+
351
+ // Dirty-checking (Finding 6): Skip redundant stdout paint updates
352
+ const cacheKey = `${key}:${currentStyle}`;
353
+ if (lastRenderedStatus.get(cacheKey) === beautified) {
354
+ return;
355
+ }
356
+ lastRenderedStatus.set(cacheKey, beautified);
357
+
259
358
  return originalSetStatus.call(ctx.ui, key, beautified);
260
359
  };
261
360
  (wrapped as any).__beautifierHooked = true;
@@ -270,7 +369,7 @@ export default function (pi: ExtensionAPI) {
270
369
  if (!ctx.hasUI || !ctx.ui) {
271
370
  return;
272
371
  }
273
-
372
+
274
373
  const styleOptions = [
275
374
  "minimal (Millennial Minimalist - e.g. name ❯ ●)",
276
375
  "apple (Apple Cupertino - e.g. name β”‚ ●)",
@@ -281,25 +380,31 @@ export default function (pi: ExtensionAPI) {
281
380
  "glass (Charm Glass - e.g. β–• name ● ▏)",
282
381
  "glow (Glow Badge - e.g. name [ ● ])",
283
382
  "matrix (Retro Matrix - e.g. name β¦— ● | 2 ⦘)",
284
- "off (use raw output)"
383
+ "off (use raw output)",
285
384
  ];
286
-
287
- const choice = await ctx.ui.select(
288
- `Choose status style (Current: ${currentStyle}):`,
289
- styleOptions
290
- );
385
+
386
+ const choice = await ctx.ui.select(`Choose status style (Current: ${currentStyle}):`, styleOptions);
291
387
 
292
388
  if (choice) {
293
389
  const styleKey = choice.split(" ")[0].trim();
294
390
  const validStyles = [
295
- "apple", "openai", "anthropic", "google", "microsoft",
296
- "minimal", "glass", "glow", "matrix", "off"
391
+ "apple",
392
+ "openai",
393
+ "anthropic",
394
+ "google",
395
+ "microsoft",
396
+ "minimal",
397
+ "glass",
398
+ "glow",
399
+ "matrix",
400
+ "off",
297
401
  ];
298
-
402
+
299
403
  if (validStyles.includes(styleKey)) {
300
404
  // 1. Instantly update in-memory caching
301
405
  currentStyle = styleKey;
302
406
  googleColorizeCache.clear(); // Clear cache when changing style/redrawing to ensure new colors are loaded
407
+ lastRenderedStatus.clear(); // Clear dirty checking cache to force repaint
303
408
  ctx.ui.notify(`TUI status style changed to: ${styleKey}`, "info");
304
409
 
305
410
  // 2. Redraw TUI indicator widgets instantly on configuration event using cached data
@@ -321,6 +426,6 @@ export default function (pi: ExtensionAPI) {
321
426
  }
322
427
  }
323
428
  }
324
- }
429
+ },
325
430
  });
326
431
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-tui-status-beautifier",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Premium brand & layout status bar beautifier for Pi terminal TUI.",
5
5
  "main": "extensions/tui-status-beautifier.ts",
6
6
  "keywords": [
@@ -11,6 +11,20 @@
11
11
  "./extensions/tui-status-beautifier.ts"
12
12
  ]
13
13
  },
14
+ "files": [
15
+ "extensions/tui-status-beautifier.ts",
16
+ "README.md",
17
+ "CHANGELOG.md",
18
+ "LICENSE"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/3ZEROS12/pi-tui-status-beautifier.git"
23
+ },
24
+ "homepage": "https://github.com/3ZEROS12/pi-tui-status-beautifier#readme",
25
+ "bugs": {
26
+ "url": "https://github.com/3ZEROS12/pi-tui-status-beautifier/issues"
27
+ },
14
28
  "peerDependencies": {
15
29
  "@earendil-works/pi-coding-agent": "*"
16
30
  },
@@ -30,4 +44,4 @@
30
44
  "typescript": "^5.3.3",
31
45
  "vitest": "^1.2.2"
32
46
  }
33
- }
47
+ }
@@ -1,77 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
- import { beautifyStatus, googleColorize } from "./tui-status-beautifier";
3
-
4
- describe("TUI Status Beautifier Code Optimization Tests", () => {
5
- const mockTheme = {
6
- fg: (color: string, text: string) => `[${color}]${text}[/color]`,
7
- };
8
-
9
- describe("ANSI Escape Sequence Stripping & Metric Extraction (Finding 1)", () => {
10
- it("should correctly clean parameterized 256-color sequences without leaves/parameters leaking", () => {
11
- const input = "\x1B[38;5;108mactive\x1B[0m (12)";
12
- const output = beautifyStatus("test-extension", input, mockTheme, "minimal");
13
-
14
- // Expected metric: 12. If parameters like 38 or 5 leaked, they might be matched as metric instead.
15
- expect(output).toContain("(12)");
16
- expect(output).not.toContain("38");
17
- expect(output).not.toContain("5");
18
- });
19
-
20
- it("should correctly clean truecolor RGB sequences", () => {
21
- const input = "\x1B[38;2;255;0;255mactive\x1B[0m (45)";
22
- const output = beautifyStatus("test-extension", input, mockTheme, "minimal");
23
-
24
- expect(output).toContain("(45)");
25
- expect(output).not.toContain("255");
26
- expect(output).not.toContain("0");
27
- });
28
- });
29
-
30
- describe("Fast-Path Optimization (Finding 2)", () => {
31
- it("should process simple text correctly and avoid unnecessary matches", () => {
32
- const output = beautifyStatus("my-plugin", "running 5/5", mockTheme, "minimal");
33
- expect(output).toContain("my-plugin");
34
- expect(output).toContain("(5/5)");
35
- });
36
- });
37
-
38
- describe("Google Colorize Memoization/Cache (Finding 5)", () => {
39
- it("should cache subsequent calls for the same text to avoid redundant mapping and theme calls", () => {
40
- const text = "plug";
41
- const fgSpy = vi.fn((color: string, text: string) => `[${color}]${text}[/color]`);
42
- const customTheme = { fg: fgSpy };
43
-
44
- const firstCall = googleColorize(text, customTheme);
45
- const secondCall = googleColorize(text, customTheme);
46
-
47
- // Verify they return the same colorized string
48
- expect(firstCall).toBe(secondCall);
49
-
50
- // Due to cache/memoization, fgSpy should only be called search length times once
51
- expect(fgSpy).toHaveBeenCalledTimes(text.length);
52
- });
53
- });
54
-
55
- describe("Layout Preset Rendering", () => {
56
- it("should render apple style", () => {
57
- const output = beautifyStatus("test", "active", mockTheme, "apple");
58
- expect(output).toContain("β”‚");
59
- expect(output).toContain("●");
60
- });
61
-
62
- it("should render openai style", () => {
63
- const output = beautifyStatus("test", "active", mockTheme, "openai");
64
- expect(output).toContain("❂");
65
- });
66
-
67
- it("should render anthropic style", () => {
68
- const output = beautifyStatus("test", "active", mockTheme, "anthropic");
69
- expect(output).toContain("✦");
70
- });
71
-
72
- it("should return raw output when style is off", () => {
73
- const output = beautifyStatus("test", "active (5)", mockTheme, "off");
74
- expect(output).toBe("active (5)");
75
- });
76
- });
77
- });