pi-tui-status-beautifier 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.
@@ -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
 
@@ -147,7 +193,8 @@ export function beautifyStatus(
147
193
  valLower.includes("✗") ||
148
194
  valLower.includes("offline") ||
149
195
  valLower.includes("error") ||
150
- valLower.includes("failed")
196
+ valLower.includes("failed") ||
197
+ valLower.includes("stopped")
151
198
  ) {
152
199
  state = "error";
153
200
  } else if (
@@ -157,7 +204,9 @@ export function beautifyStatus(
157
204
  valLower.includes("warning") ||
158
205
  valLower.includes("pause") ||
159
206
  valLower.includes("planning") ||
160
- valLower.includes("plan")
207
+ valLower.includes("plan") ||
208
+ valLower.includes("waiting") ||
209
+ valLower.includes("cleanup")
161
210
  ) {
162
211
  state = "warning";
163
212
  } else if (
@@ -165,7 +214,11 @@ export function beautifyStatus(
165
214
  valLower.includes("✅") ||
166
215
  valLower.includes("✓") ||
167
216
  valLower.includes("online") ||
168
- valLower.includes("success")
217
+ valLower.includes("success") ||
218
+ valLower.includes("ready") ||
219
+ valLower.includes("idle") ||
220
+ valLower.includes("ok") ||
221
+ valLower.includes("done")
169
222
  ) {
170
223
  state = "success";
171
224
  } else {
@@ -214,7 +267,7 @@ export function beautifyStatus(
214
267
  }
215
268
 
216
269
  const styledSep =
217
- hasThemeFg && (sep.includes("│") || sep.includes("⁝") || sep.includes("❯"))
270
+ hasThemeFg && (sep.includes("│") || sep.includes("⁝") || sep.includes("❯") || sep.includes("»"))
218
271
  ? theme.fg("dim", sep)
219
272
  : sep;
220
273
 
@@ -327,13 +380,111 @@ export function padToVisualWidth(str: string, targetWidth: number, padChar = " "
327
380
  return str + padChar.repeat(targetWidth - currentWidth);
328
381
  }
329
382
 
383
+ /**
384
+ * Full-Footer Renderer (Hooked via ctx.ui.setFooter)
385
+ * Formats working folder, Git branch, token metrics, and active model on Line 1,
386
+ * and formats all extension statuses on Line 2.
387
+ */
388
+ export function renderBeautifiedFooter(
389
+ width: number,
390
+ _tui: any,
391
+ theme: any,
392
+ footerData: any,
393
+ ctx: any,
394
+ style: string,
395
+ customCfg?: CustomStyleConfig
396
+ ): string[] {
397
+ const preset =
398
+ style === "custom"
399
+ ? (customCfg || customStyleConfig || {})
400
+ : (BUILTIN_PRESETS[style] || BUILTIN_PRESETS.minimal);
401
+
402
+ const hasThemeFg = theme && typeof theme.fg === "function";
403
+ const sep = preset.separator || (preset.brackets ? " " : " ❯ ");
404
+ const styledSep =
405
+ hasThemeFg && (sep.includes("│") || sep.includes("⁝") || sep.includes("❯") || sep.includes("»"))
406
+ ? theme.fg("dim", sep)
407
+ : sep;
408
+
409
+ // 1. Folder & Git branch
410
+ const branch = typeof footerData?.getGitBranch === "function" ? footerData.getGitBranch() : "";
411
+ const folder = path.basename(ctx?.cwd || process.cwd()) || "workspace";
412
+ const branchPart = branch ? `${folder} (${branch})` : folder;
413
+ const styledBranch = hasThemeFg ? theme.fg("dim", branchPart) : branchPart;
414
+
415
+ // 2. Model
416
+ const modelName = ctx?.model?.name || ctx?.model?.id || "";
417
+ const styledModel = modelName ? (hasThemeFg ? theme.fg("muted", modelName) : modelName) : "";
418
+
419
+ // 3. Token metrics
420
+ let input = 0, output = 0;
421
+ if (ctx?.sessionManager?.getBranch) {
422
+ try {
423
+ for (const e of ctx.sessionManager.getBranch()) {
424
+ if (e.type === "message" && e.message?.role === "assistant" && e.message?.usage) {
425
+ const u = e.message.usage;
426
+ input += u.input || 0;
427
+ output += u.output || 0;
428
+ }
429
+ }
430
+ } catch (_) {}
431
+ }
432
+ const fmt = (n: number) => (n < 1000 ? `${n}` : `${(n / 1000).toFixed(1)}k`);
433
+ const tokenStr = input > 0 || output > 0 ? `↑${fmt(input)} ↓${fmt(output)}` : "";
434
+ const styledTokens = tokenStr ? (hasThemeFg ? theme.fg("dim", tokenStr) : tokenStr) : "";
435
+
436
+ // Left & right parts of Line 1
437
+ const leftText = styledBranch;
438
+ const rightParts = [styledTokens, styledModel].filter(Boolean);
439
+ const rightText = rightParts.join(styledSep);
440
+
441
+ const leftVis = getStringWidth(leftText);
442
+ const rightVis = getStringWidth(rightText);
443
+ const padLen = Math.max(1, width - leftVis - rightVis);
444
+ const line1 = leftText + " ".repeat(padLen) + rightText;
445
+
446
+ const lines = [line1];
447
+
448
+ // Line 2: Extension status badges
449
+ const extStatuses: Array<[string, string]> = [];
450
+ if (footerData && typeof footerData.getExtensionStatuses === "function") {
451
+ try {
452
+ const map = footerData.getExtensionStatuses();
453
+ if (map && typeof map.entries === "function") {
454
+ for (const [k, v] of map.entries()) {
455
+ if (v) extStatuses.push([k, v]);
456
+ }
457
+ }
458
+ } catch (_) {}
459
+ }
460
+ if (extStatuses.length === 0) {
461
+ for (const [k, v] of originalStatuses.entries()) {
462
+ if (v) extStatuses.push([k, v]);
463
+ }
464
+ }
465
+
466
+ if (extStatuses.length > 0) {
467
+ const badges = extStatuses
468
+ .map(([k, v]) => beautifyStatus(k, v, theme, style, customCfg))
469
+ .filter(Boolean) as string[];
470
+ if (badges.length > 0) {
471
+ lines.push(badges.join(" "));
472
+ }
473
+ }
474
+
475
+ return lines;
476
+ }
477
+
330
478
  export default function (pi: ExtensionAPI) {
331
479
  pi.on("session_start", async (_event, ctx) => {
332
480
  if (!ctx.hasUI || !ctx.ui) return;
333
481
 
482
+ loadConfig(ctx.cwd || process.cwd());
483
+
334
484
  originalStatuses.clear();
335
485
  lastRenderedStatus.clear();
336
486
 
487
+ // 1. Hook setStatus for granular extension badge formatting
337
488
  const originalSetStatus = ctx.ui.setStatus;
338
489
  if (originalSetStatus && !(originalSetStatus as any).__beautifierHooked) {
339
490
  const wrapped = function (key: string, value: string | undefined) {
@@ -352,6 +503,25 @@ export default function (pi: ExtensionAPI) {
352
503
  (wrapped as any).__beautifierHooked = true;
353
504
  ctx.ui.setStatus = wrapped;
354
505
  }
506
+
507
+ // 2. Hook setFooter for full-footer beautification
508
+ if (typeof (ctx.ui as any).setFooter === "function") {
509
+ (ctx.ui as any).setFooter((tui: any, theme: any, footerData: any) => {
510
+ const unsub =
511
+ typeof footerData?.onBranchChange === "function"
512
+ ? footerData.onBranchChange(() => tui.requestRender())
513
+ : undefined;
514
+
515
+ return {
516
+ dispose: unsub,
517
+ invalidate() {},
518
+ render(width: number): string[] {
519
+ if (currentStyle === "off") return [];
520
+ return renderBeautifiedFooter(width, tui, theme, footerData, ctx, currentStyle, customStyleConfig);
521
+ },
522
+ };
523
+ });
524
+ }
355
525
  });
356
526
 
357
527
  // Dynamic status bar style controller registry
@@ -380,8 +550,20 @@ export default function (pi: ExtensionAPI) {
380
550
  const promptRecipe = [
381
551
  "You are acting as my Pi TUI Status Bar designer for `pi-tui-status-beautifier`.",
382
552
  "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."
553
+ "2. Once I select or describe my aesthetic, save the configuration directly into `./.pi/settings.json` (or `~/.pi/agent/settings.json`) under `beautifier`:",
554
+ " {",
555
+ ' "beautifier": {',
556
+ ' "style": "custom",',
557
+ ' "custom": {',
558
+ ' "separator": " » ",',
559
+ ' "brackets": ["«", "»"],',
560
+ ' "prefix": "✦ ",',
561
+ ' "suffix": "",',
562
+ ' "glyphs": { "success": "✦", "warning": "◇", "error": "✖", "accent": "✧" }',
563
+ " }",
564
+ " }",
565
+ " }",
566
+ "Do NOT crawl filesystem or edit extension source code files."
385
567
  ].join("\n");
386
568
 
387
569
  if (typeof (ctx.ui as any).setEditorText === "function") {
@@ -398,18 +580,17 @@ export default function (pi: ExtensionAPI) {
398
580
  lastRenderedStatus.clear();
399
581
  ctx.ui.notify(`TUI status style changed to: ${styleKey}`, "info");
400
582
 
583
+ const { targetSettingsPath } = loadConfig(ctx.cwd || process.cwd());
584
+
401
585
  try {
402
- if (fs.existsSync(settingsPath)) {
403
- const data = fs.readFileSync(settingsPath, "utf-8");
586
+ if (fs.existsSync(targetSettingsPath)) {
587
+ const data = fs.readFileSync(targetSettingsPath, "utf-8");
404
588
  const config = JSON.parse(data);
405
- if (config.beautifier?.custom && typeof config.beautifier.custom === "object") {
406
- customStyleConfig = config.beautifier.custom;
407
- }
408
589
  if (!config.beautifier) config.beautifier = {};
409
590
  config.beautifier.style = styleKey;
410
- const tempSettings = `${settingsPath}.tmp.${process.pid}.${Date.now()}`;
591
+ const tempSettings = `${targetSettingsPath}.tmp.${process.pid}.${Date.now()}`;
411
592
  fs.writeFileSync(tempSettings, JSON.stringify(config, null, 2), "utf-8");
412
- fs.renameSync(tempSettings, settingsPath);
593
+ fs.renameSync(tempSettings, targetSettingsPath);
413
594
  }
414
595
  } catch (_e) {
415
596
  // 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.1",
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": [