pi-tui-status-beautifier 1.7.2 → 1.7.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/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # pi-tui-status-beautifier
2
2
 
3
- > **Minimalist status bar formatter & AI-customizable layout engine for Pi TUI**
4
- > Normalizes verbose, mismatched extension status badges into clean, coherent typography with zero runtime render-loop disk I/O.
3
+ Minimalist status bar formatter & layout engine for Pi TUI.
4
+
5
+ Clean up fragmented extension status strings. Enjoy adaptive narrow-terminal folding and zero render-loop disk I/O, with zero telemetry hijacking.
5
6
 
6
7
  [![npm version](https://img.shields.io/npm/v/pi-tui-status-beautifier?color=blue)](https://www.npmjs.com/package/pi-tui-status-beautifier)
7
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
@@ -22,24 +23,58 @@
22
23
  └─────────────────────────────────────────────────────────────────────────────────────────────────────┘
23
24
  ```
24
25
 
26
+ ## Quick Start
27
+
28
+ Install directly inside Pi:
29
+
30
+ ```bash
31
+ pi install npm:pi-tui-status-beautifier
32
+ ```
33
+
34
+ Takes effect immediately. Type `/beautify` anytime to switch styles interactively.
35
+
25
36
  ---
26
37
 
27
- ## Why pi-tui-status-beautifier?
38
+ ## Core Value: Why Do Developers Need This?
28
39
 
29
- As the Pi terminal ecosystem grows, developers routinely install multiple extensions (`plannotator`, `pi-mcp-adapter`, `toolflow`, `pi-anchor`, `pi-control-chrome`, `pi-lingual`). Over time, the footer status bar deteriorates into visual chaos:
40
+ As the Pi extension ecosystem grows, developers routinely install multiple tools (`plannotator`, `pi-mcp-adapter`, `toolflow`, `pi-anchor`, `pi-lingual`). The status bar quickly runs into four frictions:
30
41
 
31
- 1. **Inconsistent Syntax & Naming**: Every extension authors status strings independently. Some prefix long package names (`pi-mcp-adapter:`), some wrap counts in brackets (`[2]`), while others dump raw emojis or status sentences. The lack of standard metrics distracts focus during coding flow.
32
- 2. **Narrow Terminal Line Wrapping**: In split-pane layouts (tmux, WezTerm, iTerm2 panes `< 80` columns), long status strings wrap onto multiple lines or tear off the edge of the screen, pushing input prompts up and down.
33
- 3. **Surrogate-Pair & CJK Misalignments**: Emojis, East Asian Wide (CJK) characters, and zero-width joiners (ZWJ) trigger width calculation discrepancies in naive string formatters, leading to flickering and cursor misalignment.
34
- 4. **Render-Loop Disk Latency**: Reading configuration files inside high-frequency TUI animation loops introduces I/O thrashing and terminal sluggishness.
42
+ ### 1. Inconsistent Syntax & Visual Clutter
43
+ Every extension authors status text independently. Some prefix long package names (`pi-mcp-adapter:`), some wrap counts in brackets (`[2]`), and others dump raw emojis. Inconsistent character metrics create visual distraction at the bottom of your screen.
44
+ `pi-tui-status-beautifier` strips package suffixes and normalizes raw text into uniform, structured micro-badges.
35
45
 
36
- `pi-tui-status-beautifier` acts as a non-intrusive normalization layer. It intercepts `ctx.ui.setStatus` calls, sanitizes ANSI and surrogate characters, dynamically maps state signals into uniform badges, and folds idle items when horizontal space tightens.
46
+ ### 2. Narrow Terminal Line Wrapping
47
+ In split-pane layouts (tmux, WezTerm, or windows `< 80` columns), long status strings wrap onto multiple lines, shoving your prompt up and down and breaking typing flow.
48
+ The built-in column budget guard automatically collapses idle badges into `(+3 idle)` when width tightens, prioritizing errors and running tasks.
49
+
50
+ ### 3. Never Hijacks Platform Telemetry
51
+ Many status bar plugins take a heavy-handed approach: completely replacing Pi's native footer via `ctx.ui.setFooter`. In doing so, they wipe out essential system metrics—git branches, token counts (`↑1.2k ↓3.4k`), cache read rates, and thinking levels (`• high`).
52
+ This extension defaults to a **non-intrusive micro-hook (`status-only`)**: it normalizes extension badges while leaving Pi's native system telemetry 100% intact.
53
+
54
+ ### 4. Zero Render-Loop Disk I/O
55
+ Reading configuration files synchronously inside high-frequency TUI animation redraw loops causes disk contention and sluggish typing.
56
+ This extension loads settings into memory at startup and uses dirty-checking caches, performing **zero synchronous file reads** during TUI redraws.
57
+
58
+ ---
59
+
60
+ ## How It Works: The Action Matrix
61
+
62
+ ```text
63
+ /beautify (Open interactive theme switcher anytime)
64
+ ```
65
+
66
+ | Mode / Scenario | What You Do | What the Beautifier Does | What You See | Best For |
67
+ | :--- | :--- | :--- | :--- | :--- |
68
+ | **`stream`** *(Default)* | Install extensions normally | Strips package prefixes and normalizes badges | `plan ❯ ◆ 3 mcp ❯ ● ready anchor ❯ ⌖ 3` | Minimalist everyday pairing |
69
+ | **Narrow Split Panes** | tmux split width `< 80` cols | Preserves active badges, condenses idle items | `toolflow ❯ ⌬ running (2/5) (+3 idle)` | Split panes without line wrap |
70
+ | **`full-footer`** *(Panorama)* | Enable full footer mode | Formats unified two-line layout | Line 1: Git/Token/Model; Line 2: Normalized badges | Geek panoramic overview |
71
+ | **`off`** *(Clean Revert)* | Run `/beautify off` | Physically deletes `beautifier` key from `settings.json` | 100% native unformatted status bar, 0 residue | Zero-regret testing |
37
72
 
38
73
  ---
39
74
 
40
75
  ## Preset Gallery & Visual Themes
41
76
 
42
- Switch styles interactively with `/beautify`:
77
+ Type `/beautify` to switch styles interactively:
43
78
 
44
79
  ```text
45
80
  1. stream (Default Minimalist Stream)
@@ -58,68 +93,34 @@ Switch styles interactively with `/beautify`:
58
93
  chrome ❯ ● ready toolflow ❯ ⌬ 2/5 anchor ❯ ⌖ 3
59
94
 
60
95
  6. custom (AI-Assisted Co-Design)
61
- Generates bespoke separators, framing glyphs, and status indicators matching your aesthetic.
96
+ Describe your aesthetic in plain text; your agent generates 3 ASCII mockups.
62
97
  ```
63
98
 
64
99
  ---
65
100
 
66
- ## Core Capabilities & Architectural Design
67
-
68
- ### 1. Dual-Scope Formatting Architecture
69
- When configuring `/beautify`, you choose between two execution scopes:
70
- * **`status-only` (Default & Recommended)**: Only hooks `ctx.ui.setStatus` badges. Leaves Pi's native Git branch, working directory, context window percentages, token telemetry (`↑1.2k ↓3.4k R8.9k`), and thinking levels (`• high`) 100% native and untouched.
71
- * **`full-footer` (Integrated Geek Footer)**: Replaces the entire bottom bar with a unified two-line layout:
72
- * **Line 1**: Folder & Git branch on the left; token metrics, context cache hit rates, model identifier, and reasoning level on the right.
73
- * **Line 2**: Normalized extension status badges with automatic column budgeting.
74
-
75
- ### 2. Adaptive Folding for Narrow Terminals (< 80 Cols)
76
- In compact split panes, status items risk overflowing terminal boundaries. `pi-tui-status-beautifier` partitions active extensions into two priority tiers:
77
- * **High-Priority (Active/Warning/Error)**: Badges displaying `running`, `working`, `starting`, `error`, `failed`, or `pause` are always preserved at full fidelity.
78
- * **Low-Priority (Idle/Ready/Ok)**: When line width exceeds maximum available columns, idle badges are automatically condensed into a compact indicator:
79
- ```text
80
- toolflow ❯ ⌬ running (2/5) plan ❯ ▲ error (+3 idle)
81
- ```
82
-
83
- ### 3. Dedicated Extension Identity & Micro-Spinners
84
- The engine strips internal package naming suffixes (`-extension`, `-plugin`, `-adapter`, `-slash-text`) and maps established ecosystem tools to clean semantic identities:
85
- * `plannotator` ➔ `plan` (accent glyph `◆`)
86
- * `pi-mcp-adapter` ➔ `mcp` (status dot `●`)
87
- * `pi-control-chrome` ➔ `chrome` (status dot `●`)
88
- * `pi-anchor` ➔ `anchor` (anchor glyph `⌖`)
89
- * `toolflow` ➔ `toolflow` (flow glyph `⌬`)
90
- * `pi-lingual` ➔ `lingual` (exchange glyph `⇄`)
91
- * In-progress tasks (`running`, `working`, `...`) automatically trigger a 300ms terminal micro-spinner (`◐`, `◓`, `◑`, `◒`).
92
-
93
- ### 4. Zero-Ceremony AI Co-Design (`/beautify custom`)
94
- Want a unique terminal aesthetic without manually editing code?
95
- 1. Run `/beautify` and select `custom`.
96
- 2. Enter your desired aesthetic in plain text (e.g. *"Cyberpunk neon dots"*, *"Nordic minimal pastels"*, *"Subtle dashed brackets"*).
97
- 3. The beautifier dispatches a structured prompt recipe to your active Coding Agent.
98
- 4. Your agent presents **3 distinct ASCII visual mockups** via interactive selection and writes the chosen configuration directly to `./.pi/settings.json`.
99
-
100
- ---
101
-
102
- ## Under-The-Hood Engineering
103
-
104
- ### Unicode UAX #11 & Surrogate-Pair Safety
105
- Terminal character counting based on `string.length` breaks when encountering emojis, Chinese/Japanese characters, or surrogate pairs. `pi-tui-status-beautifier` implements a standalone `codePointAt(0)` scanner (`src/extensions/tui-status-beautifier.ts`):
106
- * Accurately measures East Asian Wide (2 visual columns), ASCII (1 column), and zero-width markers (0 columns).
107
- * Explicitly ignores Zero-Width Joiners (`0x200d`), Emoji Variation Selectors (`0xfe0f`, `0xfe0e`), and combining diacritics (`0x0300..0x036f`).
108
- * Implements `sliceToVisualWidth` to truncate long identifiers without severing multibyte surrogate pairs.
109
-
110
- ### Zero Render-Loop Disk I/O & Dirty-Checking
111
- * **Cached Memory Reads**: Configuration is loaded into memory at startup. Normal TUI redraw loops perform **zero synchronous file reads (`fs.readFileSync`)**.
112
- * **State Dirty-Checking**: Renders are cached per badge (`${key}:${currentStyle}`). If an extension sends an identical status string, formatting is skipped, avoiding redundant string allocations.
113
- * **Atomic Settings Writes**: Style updates via `/beautify` use atomic file replacement (`writeFileSync(temp)` + `renameSync(temp, target)`), preventing corrupt settings files if the process is terminated mid-write.
101
+ ## Engineering Highlights
114
102
 
115
- ### Zero-Trace Physical Cleanup (`off`)
116
- Selecting `off` cleanly restores all original unformatted status strings to `ctx.ui.setStatus`, unhooks the footer, and deletes the `beautifier` block from `settings.json`. It leaves zero orphaned configuration schema behind.
103
+ - 🛡️ **Non-Destructive Dual Scopes**:
104
+ * `status-only` (Default & Recommended): Only hooks `ctx.ui.setStatus` badges. Preserves Pi's native Git branch, working directory, token counts, and thinking levels 100% untouched.
105
+ * `full-footer`: Formats the entire bottom bar with system telemetry on line 1 and badges on line 2.
106
+ - 📐 **Unicode UAX #11 & Surrogate-Pair Safety**: Built-in character scanner accurately measures East Asian Wide (2 cols), ASCII (1 col), and ANSI escape codes (0 cols), eliminating cursor jitter and column misalignments.
107
+ - ⚡ **Zero Render-Loop Disk I/O**: Loads configuration once into memory. Redraw loops perform zero synchronous disk reads (`fs.readFileSync`), using dirty-checking to skip duplicate renders.
108
+ - ◐ **Terminal Micro-Spinners**: Active background tasks (`running`, `working`) trigger a smooth 300ms spinner (`◐`, `◓`, `◑`, `◒`).
109
+ - 🗑️ **Zero-Trace Physical Cleanup (`off`)**: Turning the extension off restores all original status strings and deletes the `beautifier` block from `settings.json`, leaving zero orphaned configuration behind.
117
110
 
118
111
  ---
119
112
 
120
- ## Configuration Reference
113
+ ## Commands & Configuration
121
114
 
122
- Settings reside under `"beautifier"` in `./.pi/settings.json` (project-level) or `~/.pi/agent/settings.json` (global):
115
+ ### Interactive Command
116
+ ```bash
117
+ /beautify # Open interactive style picker
118
+ /beautify stream # Switch directly to stream preset
119
+ /beautify off # Revert to native status bar and delete config
120
+ ```
121
+
122
+ ### Settings Schema
123
+ Preferences persist in `./.pi/settings.json` (project-level) or `~/.pi/agent/settings.json` (global):
123
124
 
124
125
  ```json
125
126
  {
@@ -130,77 +131,34 @@ Settings reside under `"beautifier"` in `./.pi/settings.json` (project-level) or
130
131
  }
131
132
  ```
132
133
 
133
- ### Custom Style Schema (`style: "custom"`)
134
- ```json
135
- {
136
- "beautifier": {
137
- "style": "custom",
138
- "fullFooter": false,
139
- "custom": {
140
- "prefix": "",
141
- "suffix": "",
142
- "separator": " ❯ ",
143
- "brackets": ["⦗", "⦘"],
144
- "inverse": false,
145
- "padName": false,
146
- "glyphs": {
147
- "success": "●",
148
- "warning": "◌",
149
- "error": "▲",
150
- "accent": "◆"
151
- }
152
- }
153
- }
154
- }
155
- ```
156
-
157
- ---
158
-
159
- ## Installation & Usage
160
-
161
- ### Install as a Pi Extension
162
- ```bash
163
- # Recommended: Install from npm
164
- pi install npm:pi-tui-status-beautifier
165
-
166
- # Or install from GitHub
167
- pi install git:github.com/3ZEROS12/pi-tui-status-beautifier
168
- ```
169
-
170
- ### Interactive Command
171
- Type `/beautify` inside any active Pi session to open the interactive style selector:
172
- ```text
173
- /beautify
174
- ```
175
-
176
- ### Development & Testing
177
- ```bash
178
- git clone https://github.com/3ZEROS12/pi-tui-status-beautifier.git
179
- cd pi-tui-status-beautifier
180
- npm install
181
- npm test # 18/18 Vitest unit tests pass
182
- npm run lint # Zero ESLint warnings
183
- ```
184
-
185
134
  ---
186
135
 
187
136
  ## Author's Note
188
137
 
189
- The reason I created `pi-tui-status-beautifier` comes from a simple aesthetic frustration in daily terminal pairing: visual noise.
138
+ The inspiration behind `pi-tui-status-beautifier` comes from daily aesthetic frustration: visual noise.
190
139
 
191
- As I installed multiple extensions for Pi (`pi-mcp-adapter`, `plannotator`, `toolflow`, `pi-anchor`, `pi-lingual`), the status bar quickly deteriorated into a chaotic billboard. Every extension author used their own punctuation, raw emojis, and arbitrary string widths. In split-pane tmux or compact terminal windows, lines tore, right-side borders wrapped onto new rows, and the screen constantly flickered.
140
+ As I installed more extensions for Pi, the bottom status bar turned into a chaotic billboard. Every extension author used their own punctuation, raw emojis, and arbitrary string widths. In split-pane tmux or compact windows, lines wrapped awkwardly and the screen constantly flickered.
192
141
 
193
142
  When I looked into existing status bar plugins, many took a sledgehammer approach: completely replacing Pi's native footer via `ctx.ui.setFooter`. In doing so, they wiped out essential platform metrics—token usage, cache read rates, thinking levels, and git branches. Even worse, some plugins read configuration files synchronously inside the animation redraw loop, causing typing sluggishness.
194
143
 
195
144
  I built this beautifier around three strict constraints:
196
- 1. Non-destructive scoping: never hijack host platform telemetry; normalize only the extension status badges;
197
- 2. Zero render-loop disk I/O: cache settings in memory and skip duplicate renders;
198
- 3. Zero-trace revert: turning it off physically deletes the config block (`delete config.beautifier`), leaving zero psychological or schema clutter.
145
+ 1. **Never hijack native telemetry**: Normalize extension badges while keeping system metrics untouched.
146
+ 2. **Zero render-loop disk reads**: Memory-cached configuration with dirty-checking.
147
+ 3. **Zero-trace revert**: Turning it off physically deletes the config block (`delete config.beautifier`).
199
148
 
200
149
  A good status bar should be quiet, balanced, and invisible until you need it.
201
150
 
202
151
  ---
203
152
 
153
+ ## Verification & Testing
154
+
155
+ ```bash
156
+ npm test # 18/18 Vitest unit tests pass
157
+ npm run lint # Zero ESLint warnings
158
+ ```
159
+
160
+ ---
161
+
204
162
  ## License
205
163
 
206
164
  MIT © [Jason](https://github.com/3ZEROS12)
@@ -93,11 +93,11 @@ const lastRenderedStatus = new Map<string, string | undefined>();
93
93
 
94
94
  // Safe, non-backtracking ReDoS-mitigated ANSI escape sequence matcher (supporting ; and :)
95
95
  const ANSI_STRIP_REGEX =
96
- /[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-ntqry=><~]))/g;
96
+ /[\u001B\u009B][\[\]\\()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?\u0007)|(?:(?:\d{1,4}(?:[:;]\d{0,4})*)?[\dA-PR-TZcf-ntqry=><~]))/g;
97
97
 
98
98
  // Hoisted regex patterns to prevent instantiation and GC overhead in the render hot-path
99
99
  const STATUS_PATTERN =
100
- /[🟢🔴🟡⚪⏸✅❌✓✗?✔✖☑☐◆📋⌬★✦◇»«‹›⦗⦘]|[0-9]+\/[0-9]+|active|online|offline|running|paused|error|success|connected|disconnected|ready|idle|ok|done|failed|waiting|stopped|listening|cleanup|starting/i;
100
+ /[🟢🔴🟡⚪⏸✅❌✓✗?✔✖☑☐◆📋⌬★✦◇»«‹›⦗⦘]|[0-9]+\/[0-9]+|\b(?:active|online|offline|running|paused|error|success|connected|disconnected|ready|idle|ok|done|failed|waiting|stopped|listening|cleanup|starting)\b/i;
101
101
  const NAME_SUFFIX_PATTERN = /-(extension|plugin|assistant|adapter|slash-text|slash|text|widget)$/gi;
102
102
  const FRACTION_PATTERN = /(\d+\/\d+|\d+%\s*)/;
103
103
  const PARENTHESES_PATTERN = /\((\d+)\)/;
@@ -108,28 +108,42 @@ let currentStyle = "minimal";
108
108
  let fullFooterEnabled = false;
109
109
  let customStyleConfig: CustomStyleConfig | undefined;
110
110
 
111
+ function isTestEnvironment(): boolean {
112
+ return (
113
+ process.env.NODE_ENV === "test" ||
114
+ process.env.VITEST !== undefined ||
115
+ process.env.NODE_TEST_CONTEXT !== undefined ||
116
+ process.execArgv.some((a) => a.includes("test") || a.includes("vitest")) ||
117
+ process.env.npm_lifecycle_event === "test"
118
+ );
119
+ }
120
+
111
121
  /**
112
122
  * Resolves configuration hierarchically:
113
123
  * Project-level `./.pi/settings.json` takes priority over global `~/.pi/agent/settings.json`
114
124
  */
115
- export function loadConfig(cwd: string = process.cwd()): {
125
+ export function loadConfig(cwd?: string): {
116
126
  style: string;
117
127
  fullFooter: boolean;
118
128
  custom?: CustomStyleConfig;
119
129
  targetSettingsPath: string;
120
130
  } {
131
+ const effectiveCwd = cwd || process.cwd();
121
132
  const home = process.env.HOME || process.env.USERPROFILE || "";
122
133
  const globalPath = path.join(home, ".pi/agent/settings.json");
123
- const localPath = path.join(cwd, ".pi/settings.json");
134
+ const localPath = path.join(effectiveCwd, ".pi/settings.json");
124
135
 
125
136
  let resolvedStyle = "minimal";
126
137
  let resolvedFullFooter = false;
127
138
  let resolvedCustom: CustomStyleConfig | undefined;
128
139
  let hasLocalConfig = false;
129
140
 
141
+ // Multi-Probe Test Isolation: skip reading real global ~/.pi/agent/settings.json in test runner unless cwd explicitly provided
142
+ const skipGlobalInTest = isTestEnvironment() && cwd === undefined;
143
+
130
144
  // 1. Read global settings
131
145
  try {
132
- if (fs.existsSync(globalPath)) {
146
+ if (!skipGlobalInTest && fs.existsSync(globalPath)) {
133
147
  const data = JSON.parse(fs.readFileSync(globalPath, "utf-8"));
134
148
  if (data.beautifier) {
135
149
  if (typeof data.beautifier.style === "string") {
@@ -185,6 +199,37 @@ export function loadConfig(cwd: string = process.cwd()): {
185
199
 
186
200
  loadConfig();
187
201
 
202
+ function sanitizeCustomConfig(raw: any): CustomStyleConfig | undefined {
203
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
204
+ const clampStr = (v: unknown, maxLen = 16): string | undefined =>
205
+ typeof v === "string" ? v.slice(0, maxLen) : undefined;
206
+ const brackets =
207
+ Array.isArray(raw.brackets) &&
208
+ raw.brackets.length === 2 &&
209
+ typeof raw.brackets[0] === "string" &&
210
+ typeof raw.brackets[1] === "string"
211
+ ? ([raw.brackets[0].slice(0, 4), raw.brackets[1].slice(0, 4)] as [string, string])
212
+ : undefined;
213
+ const rawGlyphs = raw.glyphs && typeof raw.glyphs === "object" && !Array.isArray(raw.glyphs) ? raw.glyphs : undefined;
214
+ const glyphs = rawGlyphs
215
+ ? {
216
+ success: clampStr(rawGlyphs.success, 4),
217
+ warning: clampStr(rawGlyphs.warning, 4),
218
+ error: clampStr(rawGlyphs.error, 4),
219
+ accent: clampStr(rawGlyphs.accent, 4),
220
+ }
221
+ : undefined;
222
+ return {
223
+ prefix: clampStr(raw.prefix, 16),
224
+ suffix: clampStr(raw.suffix, 16),
225
+ separator: clampStr(raw.separator, 12),
226
+ brackets,
227
+ inverse: typeof raw.inverse === "boolean" ? raw.inverse : undefined,
228
+ padName: typeof raw.padName === "boolean" ? raw.padName : undefined,
229
+ glyphs,
230
+ };
231
+ }
232
+
188
233
  // Clean status text and map state and details
189
234
  export function beautifyStatus(
190
235
  key: string,
@@ -197,13 +242,14 @@ export function beautifyStatus(
197
242
  if (style === "off" || OFF_STYLES.has((style || "").toLowerCase().trim())) return originalVal;
198
243
 
199
244
  try {
200
- const hasAnsi = originalVal.includes("\u001B") || originalVal.includes("\u009B");
201
- const cleanVal = hasAnsi ? originalVal.replace(ANSI_STRIP_REGEX, "").trim() : originalVal.trim();
245
+ const boundedVal = originalVal.length > 512 ? originalVal.slice(0, 512) : originalVal;
246
+ const hasAnsi = boundedVal.includes("\u001B") || boundedVal.includes("\u009B");
247
+ const cleanVal = hasAnsi ? boundedVal.replace(ANSI_STRIP_REGEX, "").trim() : boundedVal.trim();
202
248
 
203
249
  if (cleanVal === "") return "";
204
250
 
205
251
  if (!STATUS_PATTERN.test(cleanVal)) {
206
- return originalVal;
252
+ return originalVal.length > 64 ? sliceToVisualWidth(cleanVal, 32) : originalVal;
207
253
  }
208
254
 
209
255
  // 1. Get display name (compact natural width by default to save precious terminal columns)
@@ -275,7 +321,6 @@ export function beautifyStatus(
275
321
  valLower.includes("warning") ||
276
322
  valLower.includes("pause") ||
277
323
  valLower.includes("planning") ||
278
- valLower.includes("plan") ||
279
324
  valLower.includes("waiting") ||
280
325
  valLower.includes("cleanup")
281
326
  ) {
@@ -291,13 +336,14 @@ export function beautifyStatus(
291
336
  valLower.includes("ok") ||
292
337
  valLower.includes("done")
293
338
  ) {
294
- state = "success";
339
+ state = name === "plan" ? "accent" : "success";
295
340
  } else {
296
341
  state = name === "plan" ? "accent" : "success";
297
342
  }
298
343
 
299
344
  // 4. Determine state glyph & indicator
300
345
  const glyphMap = preset.glyphs || {};
346
+ const isEcosystemGlyph = name === "anchor" || name === "toolflow" || name === "lingual";
301
347
  let defaultGlyph =
302
348
  name === "anchor"
303
349
  ? "⌖"
@@ -314,18 +360,21 @@ export function beautifyStatus(
314
360
  : "●";
315
361
 
316
362
  // Dynamic micro-spinner for active in-progress / running states (running, working, starting, waiting)
317
- if (
363
+ const isSpinning =
318
364
  valLower.includes("running") ||
319
365
  valLower.includes("working") ||
320
366
  valLower.includes("starting") ||
321
367
  valLower.includes("waiting") ||
322
- valLower.includes("...")
323
- ) {
368
+ valLower.includes("...");
369
+ if (isSpinning) {
324
370
  const spinnerChar = SPINNERS[Math.floor(Date.now() / 300) % SPINNERS.length];
325
371
  defaultGlyph = spinnerChar;
326
372
  }
327
373
 
328
- let glyph = glyphMap[state] || defaultGlyph;
374
+ // Preserve ecosystem glyphs (⌖, ⌬, ⇄) and active spinners across all preset themes unless overridden in custom style
375
+ const glyph = (isSpinning || (isEcosystemGlyph && style !== "custom"))
376
+ ? defaultGlyph
377
+ : (glyphMap[state] || defaultGlyph);
329
378
 
330
379
  const hasThemeFg = theme && typeof theme.fg === "function";
331
380
  const coloredGlyph = hasThemeFg ? theme.fg(state, glyph) : glyph;
@@ -368,6 +417,14 @@ export function beautifyStatus(
368
417
  }
369
418
  }
370
419
 
420
+ const isZeroWidthCode = (code: number) =>
421
+ code === 0x200d ||
422
+ code === 0xfe0f ||
423
+ code === 0xfe0e ||
424
+ (code >= 0x0300 && code <= 0x036f) ||
425
+ (code >= 0x1f3fb && code <= 0x1f3ff) ||
426
+ (code >= 0x200b && code <= 0x200f);
427
+
371
428
  /**
372
429
  * Calculates the visual column cell width of a string, taking into account
373
430
  * full-width Unicode characters, East Asian ideographs, Kana, Hangul, emojis, and zero-width joiners.
@@ -376,17 +433,20 @@ export function getStringWidth(str: string): number {
376
433
  if (!str) return 0;
377
434
  const clean = str.replace(ANSI_STRIP_REGEX, "");
378
435
  let width = 0;
436
+ let afterZwj = false;
379
437
 
380
438
  for (const char of clean) {
381
439
  const code = char.codePointAt(0) || 0;
382
440
 
383
- if (
384
- code === 0x200d ||
385
- code === 0xfe0f ||
386
- code === 0xfe0e ||
387
- (code >= 0x0300 && code <= 0x036f) ||
388
- (code >= 0x200b && code <= 0x200f)
389
- ) {
441
+ if (code === 0x200d) {
442
+ afterZwj = true;
443
+ continue;
444
+ }
445
+ if (isZeroWidthCode(code)) {
446
+ continue;
447
+ }
448
+ if (afterZwj) {
449
+ afterZwj = false;
390
450
  continue;
391
451
  }
392
452
 
@@ -434,21 +494,27 @@ export function isFullWidth(code: number): boolean {
434
494
  * Slices a string to a maximum visual cell width without cutting surrogate pairs in half.
435
495
  */
436
496
  export function sliceToVisualWidth(str: string, maxWidth: number): string {
497
+ if (!str || maxWidth <= 0) return "";
437
498
  let width = 0;
438
499
  let sliced = "";
500
+ let pendingZwj = false;
439
501
 
440
502
  for (const char of str) {
441
503
  const code = char.codePointAt(0) || 0;
442
- const charWidth =
443
- code === 0x200d ||
444
- code === 0xfe0f ||
445
- code === 0xfe0e ||
446
- (code >= 0x0300 && code <= 0x036f) ||
447
- (code >= 0x200b && code <= 0x200f)
448
- ? 0
449
- : isFullWidth(code)
450
- ? 2
451
- : 1;
504
+ if (code === 0x200d) {
505
+ if (sliced.length > 0) pendingZwj = true;
506
+ continue;
507
+ }
508
+ if (isZeroWidthCode(code)) {
509
+ if (sliced.length > 0) sliced += char;
510
+ continue;
511
+ }
512
+ if (pendingZwj) {
513
+ sliced += "\u200d" + char;
514
+ pendingZwj = false;
515
+ continue;
516
+ }
517
+ const charWidth = isFullWidth(code) ? 2 : 1;
452
518
 
453
519
  if (width + charWidth > maxWidth) {
454
520
  break;
@@ -539,11 +605,11 @@ export function renderBeautifiedFooter(
539
605
  ): string[] {
540
606
  const preset =
541
607
  style === "custom"
542
- ? (customCfg || customStyleConfig || {})
608
+ ? (sanitizeCustomConfig(customCfg) || sanitizeCustomConfig(customStyleConfig) || {})
543
609
  : (BUILTIN_PRESETS[style] || BUILTIN_PRESETS.minimal);
544
610
 
545
611
  const hasThemeFg = theme && typeof theme.fg === "function";
546
- const sep = preset.separator || (preset.brackets ? " " : " ❯ ");
612
+ const sep = typeof preset.separator === "string" && preset.separator ? preset.separator : (preset.brackets ? " " : " ❯ ");
547
613
  const styledSep =
548
614
  hasThemeFg && (sep.includes("│") || sep.includes("⁝") || sep.includes("❯") || sep.includes("»"))
549
615
  ? theme.fg("dim", sep)
@@ -563,9 +629,13 @@ export function renderBeautifiedFooter(
563
629
  let input = 0, output = 0, cacheRead = 0, cacheWrite = 0, cost = 0;
564
630
  let latestCacheHitRate: number | undefined;
565
631
 
566
- if (ctx?.sessionManager?.getEntries) {
632
+ const entriesSource =
633
+ ctx?.sessionManager?.getEntries?.() ||
634
+ ctx?.sessionManager?.getBranch?.();
635
+
636
+ if (entriesSource) {
567
637
  try {
568
- for (const entry of ctx.sessionManager.getEntries()) {
638
+ for (const entry of entriesSource) {
569
639
  if (entry.type === "usage" && entry.usage) {
570
640
  input += entry.usage.input || 0;
571
641
  output += entry.usage.output || 0;
@@ -845,16 +915,16 @@ export default function (pi: ExtensionAPI) {
845
915
  // Write atomically to settings.json
846
916
  const { targetSettingsPath } = loadConfig(ctx.cwd || process.cwd());
847
917
  try {
848
- if (fs.existsSync(targetSettingsPath)) {
849
- const data = fs.readFileSync(targetSettingsPath, "utf-8");
850
- const config = JSON.parse(data);
851
- if (!config.beautifier) config.beautifier = {};
852
- config.beautifier.style = styleKey;
853
- config.beautifier.fullFooter = isFullFooter;
854
- const tempSettings = `${targetSettingsPath}.tmp.${process.pid}.${Date.now()}`;
855
- fs.writeFileSync(tempSettings, JSON.stringify(config, null, 2), "utf-8");
856
- fs.renameSync(tempSettings, targetSettingsPath);
857
- }
918
+ fs.mkdirSync(path.dirname(targetSettingsPath), { recursive: true });
919
+ const config = fs.existsSync(targetSettingsPath)
920
+ ? JSON.parse(fs.readFileSync(targetSettingsPath, "utf-8"))
921
+ : {};
922
+ if (!config.beautifier) config.beautifier = {};
923
+ config.beautifier.style = styleKey;
924
+ config.beautifier.fullFooter = isFullFooter;
925
+ const tempSettings = `${targetSettingsPath}.tmp.${process.pid}.${Date.now()}`;
926
+ fs.writeFileSync(tempSettings, JSON.stringify(config, null, 2), "utf-8");
927
+ fs.renameSync(tempSettings, targetSettingsPath);
858
928
  } catch (_e) {
859
929
  // Silently ignore disk write errors
860
930
  }
@@ -887,32 +957,6 @@ export default function (pi: ExtensionAPI) {
887
957
  `TUI status style changed to: ${styleKey} (${isFullFooter ? "Full-Footer" : "Status Only"})`,
888
958
  "info"
889
959
  );
890
-
891
- if ((VALID_STYLES as readonly string[]).includes(styleKey)) {
892
- currentStyle = styleKey;
893
- lastRenderedStatus.clear();
894
- ctx.ui.notify(`TUI status style changed to: ${styleKey}`, "info");
895
-
896
- const { targetSettingsPath } = loadConfig(ctx.cwd || process.cwd());
897
-
898
- try {
899
- if (fs.existsSync(targetSettingsPath)) {
900
- const data = fs.readFileSync(targetSettingsPath, "utf-8");
901
- const config = JSON.parse(data);
902
- if (!config.beautifier) config.beautifier = {};
903
- config.beautifier.style = styleKey;
904
- const tempSettings = `${targetSettingsPath}.tmp.${process.pid}.${Date.now()}`;
905
- fs.writeFileSync(tempSettings, JSON.stringify(config, null, 2), "utf-8");
906
- fs.renameSync(tempSettings, targetSettingsPath);
907
- }
908
- } catch (_e) {
909
- // Silently ignore disk write errors
910
- }
911
-
912
- for (const [key, val] of originalStatuses.entries()) {
913
- ctx.ui.setStatus(key, val);
914
- }
915
- }
916
960
  },
917
961
  });
918
962
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-tui-status-beautifier",
3
- "version": "1.7.2",
4
- "description": "Minimalist status bar formatter & AI-customizable layout engine for Pi TUI.",
3
+ "version": "1.7.4",
4
+ "description": "Clean up your Pi terminal status bar — formats extension badges with crisp presets (Stream, Powerline, Matrix, Glow), folds idle items on narrow screens, and adds live spinners.",
5
5
  "main": "extensions/tui-status-beautifier.ts",
6
6
  "keywords": [
7
7
  "pi-package"