pi-tui-status-beautifier 1.3.0 → 1.3.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.
@@ -11,6 +11,9 @@ const COMMON_NAMES: Record<string, string> = {
11
11
  "subagent-slash-text": "subagent",
12
12
  "pi-wechat-assistant": "wechat",
13
13
  "wechat-assistant": "wechat",
14
+ "pi-control-chrome": "chrome",
15
+ chrome: "chrome",
16
+ toolflow: "toolflow",
14
17
  };
15
18
 
16
19
  export interface CustomStyleConfig {
@@ -29,17 +32,17 @@ export interface CustomStyleConfig {
29
32
 
30
33
  const BUILTIN_PRESETS: Record<string, CustomStyleConfig> = {
31
34
  minimal: {
32
- separator: " ❯ "
35
+ separator: " ❯ ",
33
36
  },
34
37
  glass: {
35
38
  prefix: "▕ ",
36
39
  suffix: " ▏",
37
- separator: " "
40
+ separator: " ",
38
41
  },
39
42
  glow: {
40
43
  brackets: ["[", "]"],
41
- inverse: true
42
- }
44
+ inverse: true,
45
+ },
43
46
  };
44
47
 
45
48
  const VALID_STYLES = ["minimal", "glass", "glow", "custom", "off"] as const;
@@ -56,36 +59,76 @@ const ANSI_STRIP_REGEX =
56
59
 
57
60
  // Hoisted regex patterns to prevent instantiation and GC overhead in the render hot-path
58
61
  const STATUS_PATTERN =
59
- /[🟢🔴🟡⚪⏸✅❌✓✗?✔✖☑☐◆📋⌬]|[0-9]+\/[0-9]+|active|online|offline|running|paused|error|success|connected|disconnected/i;
62
+ /[🟢🔴🟡⚪⏸✅❌✓✗?✔✖☑☐◆📋⌬★✦◇»«‹›⦗⦘]|[0-9]+\/[0-9]+|active|online|offline|running|paused|error|success|connected|disconnected|ready|idle|ok|done|failed|waiting|stopped|listening|cleanup|starting/i;
60
63
  const NAME_SUFFIX_PATTERN = /-(extension|plugin|assistant|adapter|slash-text|slash|text|widget)$/gi;
61
64
  const FRACTION_PATTERN = /(\d+\/\d+|\d+%\s*)/;
62
65
  const PARENTHESES_PATTERN = /\((\d+)\)/;
63
66
  const BRACKET_PATTERN = /\[(\d+)\]/;
64
67
  const NUMBER_PATTERN = /\b(\d+)\b/;
65
68
 
66
- // Load settings path
67
- const home = process.env.HOME || process.env.USERPROFILE || "";
68
- const settingsPath = path.join(home, ".pi/agent/settings.json");
69
-
70
69
  let currentStyle = "minimal";
71
70
  let customStyleConfig: CustomStyleConfig | undefined;
72
71
 
73
- try {
74
- if (fs.existsSync(settingsPath)) {
75
- const data = fs.readFileSync(settingsPath, "utf-8");
76
- const config = JSON.parse(data);
77
- if (config.beautifier && typeof config.beautifier.style === "string") {
78
- const saved = config.beautifier.style;
79
- currentStyle = (VALID_STYLES as readonly string[]).includes(saved) ? saved : "minimal";
72
+ /**
73
+ * Resolves configuration hierarchically:
74
+ * Project-level `./.pi/settings.json` takes priority over global `~/.pi/agent/settings.json`
75
+ */
76
+ export function loadConfig(cwd: string = process.cwd()): {
77
+ style: string;
78
+ custom?: CustomStyleConfig;
79
+ targetSettingsPath: string;
80
+ } {
81
+ const home = process.env.HOME || process.env.USERPROFILE || "";
82
+ const globalPath = path.join(home, ".pi/agent/settings.json");
83
+ const localPath = path.join(cwd, ".pi/settings.json");
84
+
85
+ let resolvedStyle = "minimal";
86
+ let resolvedCustom: CustomStyleConfig | undefined;
87
+ let hasLocalConfig = false;
88
+
89
+ // 1. Read global settings
90
+ try {
91
+ if (fs.existsSync(globalPath)) {
92
+ const data = JSON.parse(fs.readFileSync(globalPath, "utf-8"));
93
+ if (data.beautifier) {
94
+ if (typeof data.beautifier.style === "string") {
95
+ const s = data.beautifier.style;
96
+ if ((VALID_STYLES as readonly string[]).includes(s)) resolvedStyle = s;
97
+ }
98
+ if (data.beautifier.custom && typeof data.beautifier.custom === "object") {
99
+ resolvedCustom = data.beautifier.custom;
100
+ }
101
+ }
80
102
  }
81
- if (config.beautifier?.custom && typeof config.beautifier.custom === "object") {
82
- customStyleConfig = config.beautifier.custom;
103
+ } catch (_) {}
104
+
105
+ // 2. Project-level override (inherits and overrides global)
106
+ try {
107
+ if (fs.existsSync(localPath)) {
108
+ const data = JSON.parse(fs.readFileSync(localPath, "utf-8"));
109
+ if (data.beautifier) {
110
+ hasLocalConfig = true;
111
+ if (typeof data.beautifier.style === "string") {
112
+ const s = data.beautifier.style;
113
+ if ((VALID_STYLES as readonly string[]).includes(s)) resolvedStyle = s;
114
+ }
115
+ if (data.beautifier.custom && typeof data.beautifier.custom === "object") {
116
+ resolvedCustom = { ...(resolvedCustom || {}), ...data.beautifier.custom };
117
+ }
118
+ }
83
119
  }
84
- }
85
- } catch (_e) {
86
- // Graceful fallback to default in case of JSON parse or read errors
120
+ } catch (_) {}
121
+
122
+ currentStyle = resolvedStyle;
123
+ customStyleConfig = resolvedCustom;
124
+
125
+ // Target path for atomic write: local project if exists or in project directory, otherwise global
126
+ const targetSettingsPath = hasLocalConfig || fs.existsSync(localPath) ? localPath : globalPath;
127
+ return { style: resolvedStyle, custom: resolvedCustom, targetSettingsPath };
87
128
  }
88
129
 
130
+ loadConfig();
131
+
89
132
  // Clean status text and map state and details
90
133
  export function beautifyStatus(
91
134
  key: string,
@@ -117,6 +160,9 @@ export function beautifyStatus(
117
160
  name = name.slice(lastSlash + 1);
118
161
  }
119
162
  name = name.replace(NAME_SUFFIX_PATTERN, "").toLowerCase();
163
+ if (COMMON_NAMES[name]) {
164
+ name = COMMON_NAMES[name];
165
+ }
120
166
  }
121
167
  const visualName = padToVisualWidth(sliceToVisualWidth(name, 10), 10);
122
168
 
@@ -133,6 +179,14 @@ export function beautifyStatus(
133
179
  const numberMatch = cleanVal.match(NUMBER_PATTERN);
134
180
  if (numberMatch) {
135
181
  details = numberMatch[1];
182
+ } else {
183
+ // If no numeric metric exists, extract the status word descriptor (e.g. "ready", "idle", "cleanup")
184
+ const wordMatch = cleanVal.match(
185
+ /(?::\s*|\b)(ready|idle|ok|done|failed|waiting|cleanup|active|online|offline|running|paused|error|success|connected|disconnected)\b/i
186
+ );
187
+ if (wordMatch) {
188
+ details = wordMatch[1].toLowerCase();
189
+ }
136
190
  }
137
191
  }
138
192
  }
@@ -147,7 +201,8 @@ export function beautifyStatus(
147
201
  valLower.includes("✗") ||
148
202
  valLower.includes("offline") ||
149
203
  valLower.includes("error") ||
150
- valLower.includes("failed")
204
+ valLower.includes("failed") ||
205
+ valLower.includes("stopped")
151
206
  ) {
152
207
  state = "error";
153
208
  } else if (
@@ -157,7 +212,9 @@ export function beautifyStatus(
157
212
  valLower.includes("warning") ||
158
213
  valLower.includes("pause") ||
159
214
  valLower.includes("planning") ||
160
- valLower.includes("plan")
215
+ valLower.includes("plan") ||
216
+ valLower.includes("waiting") ||
217
+ valLower.includes("cleanup")
161
218
  ) {
162
219
  state = "warning";
163
220
  } else if (
@@ -165,7 +222,11 @@ export function beautifyStatus(
165
222
  valLower.includes("✅") ||
166
223
  valLower.includes("✓") ||
167
224
  valLower.includes("online") ||
168
- valLower.includes("success")
225
+ valLower.includes("success") ||
226
+ valLower.includes("ready") ||
227
+ valLower.includes("idle") ||
228
+ valLower.includes("ok") ||
229
+ valLower.includes("done")
169
230
  ) {
170
231
  state = "success";
171
232
  } else {
@@ -214,7 +275,7 @@ export function beautifyStatus(
214
275
  }
215
276
 
216
277
  const styledSep =
217
- hasThemeFg && (sep.includes("│") || sep.includes("⁝") || sep.includes("❯"))
278
+ hasThemeFg && (sep.includes("│") || sep.includes("⁝") || sep.includes("❯") || sep.includes("»"))
218
279
  ? theme.fg("dim", sep)
219
280
  : sep;
220
281
 
@@ -327,13 +388,111 @@ export function padToVisualWidth(str: string, targetWidth: number, padChar = " "
327
388
  return str + padChar.repeat(targetWidth - currentWidth);
328
389
  }
329
390
 
391
+ /**
392
+ * Full-Footer Renderer (Hooked via ctx.ui.setFooter)
393
+ * Formats working folder, Git branch, token metrics, and active model on Line 1,
394
+ * and formats all extension statuses on Line 2.
395
+ */
396
+ export function renderBeautifiedFooter(
397
+ width: number,
398
+ _tui: any,
399
+ theme: any,
400
+ footerData: any,
401
+ ctx: any,
402
+ style: string,
403
+ customCfg?: CustomStyleConfig
404
+ ): string[] {
405
+ const preset =
406
+ style === "custom"
407
+ ? (customCfg || customStyleConfig || {})
408
+ : (BUILTIN_PRESETS[style] || BUILTIN_PRESETS.minimal);
409
+
410
+ const hasThemeFg = theme && typeof theme.fg === "function";
411
+ const sep = preset.separator || (preset.brackets ? " " : " ❯ ");
412
+ const styledSep =
413
+ hasThemeFg && (sep.includes("│") || sep.includes("⁝") || sep.includes("❯") || sep.includes("»"))
414
+ ? theme.fg("dim", sep)
415
+ : sep;
416
+
417
+ // 1. Folder & Git branch
418
+ const branch = typeof footerData?.getGitBranch === "function" ? footerData.getGitBranch() : "";
419
+ const folder = path.basename(ctx?.cwd || process.cwd()) || "workspace";
420
+ const branchPart = branch ? `${folder} (${branch})` : folder;
421
+ const styledBranch = hasThemeFg ? theme.fg("dim", branchPart) : branchPart;
422
+
423
+ // 2. Model
424
+ const modelName = ctx?.model?.name || ctx?.model?.id || "";
425
+ const styledModel = modelName ? (hasThemeFg ? theme.fg("muted", modelName) : modelName) : "";
426
+
427
+ // 3. Token metrics
428
+ let input = 0, output = 0;
429
+ if (ctx?.sessionManager?.getBranch) {
430
+ try {
431
+ for (const e of ctx.sessionManager.getBranch()) {
432
+ if (e.type === "message" && e.message?.role === "assistant" && e.message?.usage) {
433
+ const u = e.message.usage;
434
+ input += u.input || 0;
435
+ output += u.output || 0;
436
+ }
437
+ }
438
+ } catch (_) {}
439
+ }
440
+ const fmt = (n: number) => (n < 1000 ? `${n}` : `${(n / 1000).toFixed(1)}k`);
441
+ const tokenStr = input > 0 || output > 0 ? `↑${fmt(input)} ↓${fmt(output)}` : "";
442
+ const styledTokens = tokenStr ? (hasThemeFg ? theme.fg("dim", tokenStr) : tokenStr) : "";
443
+
444
+ // Left & right parts of Line 1
445
+ const leftText = styledBranch;
446
+ const rightParts = [styledTokens, styledModel].filter(Boolean);
447
+ const rightText = rightParts.join(styledSep);
448
+
449
+ const leftVis = getStringWidth(leftText);
450
+ const rightVis = getStringWidth(rightText);
451
+ const padLen = Math.max(1, width - leftVis - rightVis);
452
+ const line1 = leftText + " ".repeat(padLen) + rightText;
453
+
454
+ const lines = [line1];
455
+
456
+ // Line 2: Extension status badges
457
+ const extStatuses: Array<[string, string]> = [];
458
+ if (footerData && typeof footerData.getExtensionStatuses === "function") {
459
+ try {
460
+ const map = footerData.getExtensionStatuses();
461
+ if (map && typeof map.entries === "function") {
462
+ for (const [k, v] of map.entries()) {
463
+ if (v) extStatuses.push([k, v]);
464
+ }
465
+ }
466
+ } catch (_) {}
467
+ }
468
+ if (extStatuses.length === 0) {
469
+ for (const [k, v] of originalStatuses.entries()) {
470
+ if (v) extStatuses.push([k, v]);
471
+ }
472
+ }
473
+
474
+ if (extStatuses.length > 0) {
475
+ const badges = extStatuses
476
+ .map(([k, v]) => beautifyStatus(k, v, theme, style, customCfg))
477
+ .filter(Boolean) as string[];
478
+ if (badges.length > 0) {
479
+ lines.push(badges.join(" "));
480
+ }
481
+ }
482
+
483
+ return lines;
484
+ }
485
+
330
486
  export default function (pi: ExtensionAPI) {
331
487
  pi.on("session_start", async (_event, ctx) => {
332
488
  if (!ctx.hasUI || !ctx.ui) return;
333
489
 
490
+ loadConfig(ctx.cwd || process.cwd());
491
+
334
492
  originalStatuses.clear();
335
493
  lastRenderedStatus.clear();
336
494
 
495
+ // Hook setStatus for clean status badge formatting (never hijack Pi's native footer)
337
496
  const originalSetStatus = ctx.ui.setStatus;
338
497
  if (originalSetStatus && !(originalSetStatus as any).__beautifierHooked) {
339
498
  const wrapped = function (key: string, value: string | undefined) {
@@ -380,8 +539,20 @@ export default function (pi: ExtensionAPI) {
380
539
  const promptRecipe = [
381
540
  "You are acting as my Pi TUI Status Bar designer for `pi-tui-status-beautifier`.",
382
541
  "1. In my primary language, call `ask_user_question` with ASCII visual preview mockups (e.g. Cyber Powerline ‹●›, Nordic Minimalist │ ◈, Capsule Pill [●], Tokyo Night » ✦) to consult my visual taste.",
383
- "2. Once I select or describe my aesthetic, save the configuration directly into `settings.json` under `beautifier.style = 'custom'` and `beautifier.custom = { separator, brackets, glyphs, prefix, suffix }`.",
384
- "Do NOT edit any extension source code files."
542
+ "2. Once I select or describe my aesthetic, save the configuration directly into `./.pi/settings.json` (or `~/.pi/agent/settings.json`) under `beautifier`:",
543
+ " {",
544
+ ' "beautifier": {',
545
+ ' "style": "custom",',
546
+ ' "custom": {',
547
+ ' "separator": " » ",',
548
+ ' "brackets": ["«", "»"],',
549
+ ' "prefix": "✦ ",',
550
+ ' "suffix": "",',
551
+ ' "glyphs": { "success": "✦", "warning": "◇", "error": "✖", "accent": "✧" }',
552
+ " }",
553
+ " }",
554
+ " }",
555
+ "Do NOT crawl filesystem or edit extension source code files."
385
556
  ].join("\n");
386
557
 
387
558
  if (typeof (ctx.ui as any).setEditorText === "function") {
@@ -398,18 +569,17 @@ export default function (pi: ExtensionAPI) {
398
569
  lastRenderedStatus.clear();
399
570
  ctx.ui.notify(`TUI status style changed to: ${styleKey}`, "info");
400
571
 
572
+ const { targetSettingsPath } = loadConfig(ctx.cwd || process.cwd());
573
+
401
574
  try {
402
- if (fs.existsSync(settingsPath)) {
403
- const data = fs.readFileSync(settingsPath, "utf-8");
575
+ if (fs.existsSync(targetSettingsPath)) {
576
+ const data = fs.readFileSync(targetSettingsPath, "utf-8");
404
577
  const config = JSON.parse(data);
405
- if (config.beautifier?.custom && typeof config.beautifier.custom === "object") {
406
- customStyleConfig = config.beautifier.custom;
407
- }
408
578
  if (!config.beautifier) config.beautifier = {};
409
579
  config.beautifier.style = styleKey;
410
- const tempSettings = `${settingsPath}.tmp.${process.pid}.${Date.now()}`;
580
+ const tempSettings = `${targetSettingsPath}.tmp.${process.pid}.${Date.now()}`;
411
581
  fs.writeFileSync(tempSettings, JSON.stringify(config, null, 2), "utf-8");
412
- fs.renameSync(tempSettings, settingsPath);
582
+ fs.renameSync(tempSettings, targetSettingsPath);
413
583
  }
414
584
  } catch (_e) {
415
585
  // Silently ignore disk write errors
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-tui-status-beautifier",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Minimalist status bar formatter & AI-customizable layout engine for Pi TUI.",
5
5
  "main": "extensions/tui-status-beautifier.ts",
6
6
  "keywords": [