pi-tui-status-beautifier 1.3.2 → 1.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.
@@ -22,6 +22,7 @@ export interface CustomStyleConfig {
22
22
  separator?: string;
23
23
  brackets?: [string, string];
24
24
  inverse?: boolean;
25
+ padName?: boolean;
25
26
  glyphs?: {
26
27
  success?: string;
27
28
  warning?: string;
@@ -31,21 +32,52 @@ export interface CustomStyleConfig {
31
32
  }
32
33
 
33
34
  const BUILTIN_PRESETS: Record<string, CustomStyleConfig> = {
34
- minimal: {
35
+ stream: {
35
36
  separator: " ❯ ",
36
37
  },
37
- glass: {
38
- prefix: "▕ ",
39
- suffix: " ▏",
38
+ powerline: {
39
+ brackets: ["‹", "›"],
40
40
  separator: " ",
41
+ glyphs: {
42
+ success: "◆",
43
+ warning: "◌",
44
+ error: "▲",
45
+ accent: "◈",
46
+ },
47
+ },
48
+ matrix: {
49
+ brackets: ["⦗", "⦘"],
50
+ separator: " ",
51
+ glyphs: {
52
+ success: "✦",
53
+ warning: "◍",
54
+ error: "✖",
55
+ accent: "✧",
56
+ },
41
57
  },
42
58
  glow: {
43
59
  brackets: ["[", "]"],
44
60
  inverse: true,
45
61
  },
62
+ // Backward-compatible aliases
63
+ minimal: {
64
+ separator: " ❯ ",
65
+ padName: true,
66
+ },
67
+ glass: {
68
+ brackets: ["‹", "›"],
69
+ separator: " ",
70
+ glyphs: {
71
+ success: "◆",
72
+ warning: "◌",
73
+ error: "▲",
74
+ accent: "◈",
75
+ },
76
+ },
46
77
  };
47
78
 
48
- const VALID_STYLES = ["minimal", "glass", "glow", "custom", "off"] as const;
79
+ const VALID_STYLES = ["stream", "powerline", "matrix", "glow", "minimal", "glass", "custom", "off"] as const;
80
+ const OFF_STYLES = new Set(["off", "default", "none", "raw", "vanilla", "disable", "disabled"]);
49
81
 
50
82
  // Store original statuses to allow dynamic redrawing on configuration changes.
51
83
  const originalStatuses = new Map<string, string | undefined>();
@@ -67,6 +99,7 @@ const BRACKET_PATTERN = /\[(\d+)\]/;
67
99
  const NUMBER_PATTERN = /\b(\d+)\b/;
68
100
 
69
101
  let currentStyle = "minimal";
102
+ let fullFooterEnabled = false;
70
103
  let customStyleConfig: CustomStyleConfig | undefined;
71
104
 
72
105
  /**
@@ -75,6 +108,7 @@ let customStyleConfig: CustomStyleConfig | undefined;
75
108
  */
76
109
  export function loadConfig(cwd: string = process.cwd()): {
77
110
  style: string;
111
+ fullFooter: boolean;
78
112
  custom?: CustomStyleConfig;
79
113
  targetSettingsPath: string;
80
114
  } {
@@ -83,6 +117,7 @@ export function loadConfig(cwd: string = process.cwd()): {
83
117
  const localPath = path.join(cwd, ".pi/settings.json");
84
118
 
85
119
  let resolvedStyle = "minimal";
120
+ let resolvedFullFooter = false;
86
121
  let resolvedCustom: CustomStyleConfig | undefined;
87
122
  let hasLocalConfig = false;
88
123
 
@@ -92,8 +127,15 @@ export function loadConfig(cwd: string = process.cwd()): {
92
127
  const data = JSON.parse(fs.readFileSync(globalPath, "utf-8"));
93
128
  if (data.beautifier) {
94
129
  if (typeof data.beautifier.style === "string") {
95
- const s = data.beautifier.style;
96
- if ((VALID_STYLES as readonly string[]).includes(s)) resolvedStyle = s;
130
+ const s = data.beautifier.style.toLowerCase().trim();
131
+ if (OFF_STYLES.has(s)) {
132
+ resolvedStyle = "off";
133
+ } else if ((VALID_STYLES as readonly string[]).includes(s)) {
134
+ resolvedStyle = s;
135
+ }
136
+ }
137
+ if (typeof data.beautifier.fullFooter === "boolean") {
138
+ resolvedFullFooter = data.beautifier.fullFooter;
97
139
  }
98
140
  if (data.beautifier.custom && typeof data.beautifier.custom === "object") {
99
141
  resolvedCustom = data.beautifier.custom;
@@ -109,8 +151,15 @@ export function loadConfig(cwd: string = process.cwd()): {
109
151
  if (data.beautifier) {
110
152
  hasLocalConfig = true;
111
153
  if (typeof data.beautifier.style === "string") {
112
- const s = data.beautifier.style;
113
- if ((VALID_STYLES as readonly string[]).includes(s)) resolvedStyle = s;
154
+ const s = data.beautifier.style.toLowerCase().trim();
155
+ if (OFF_STYLES.has(s)) {
156
+ resolvedStyle = "off";
157
+ } else if ((VALID_STYLES as readonly string[]).includes(s)) {
158
+ resolvedStyle = s;
159
+ }
160
+ }
161
+ if (typeof data.beautifier.fullFooter === "boolean") {
162
+ resolvedFullFooter = data.beautifier.fullFooter;
114
163
  }
115
164
  if (data.beautifier.custom && typeof data.beautifier.custom === "object") {
116
165
  resolvedCustom = { ...(resolvedCustom || {}), ...data.beautifier.custom };
@@ -120,11 +169,12 @@ export function loadConfig(cwd: string = process.cwd()): {
120
169
  } catch (_) {}
121
170
 
122
171
  currentStyle = resolvedStyle;
172
+ fullFooterEnabled = resolvedFullFooter;
123
173
  customStyleConfig = resolvedCustom;
124
174
 
125
175
  // Target path for atomic write: local project if exists or in project directory, otherwise global
126
176
  const targetSettingsPath = hasLocalConfig || fs.existsSync(localPath) ? localPath : globalPath;
127
- return { style: resolvedStyle, custom: resolvedCustom, targetSettingsPath };
177
+ return { style: resolvedStyle, fullFooter: resolvedFullFooter, custom: resolvedCustom, targetSettingsPath };
128
178
  }
129
179
 
130
180
  loadConfig();
@@ -138,7 +188,7 @@ export function beautifyStatus(
138
188
  customCfg?: CustomStyleConfig
139
189
  ): string | undefined {
140
190
  if (originalVal === undefined) return undefined;
141
- if (style === "off") return originalVal;
191
+ if (style === "off" || OFF_STYLES.has((style || "").toLowerCase().trim())) return originalVal;
142
192
 
143
193
  try {
144
194
  const hasAnsi = originalVal.includes("\u001B") || originalVal.includes("\u009B");
@@ -150,7 +200,7 @@ export function beautifyStatus(
150
200
  return originalVal;
151
201
  }
152
202
 
153
- // 1. Get display name
203
+ // 1. Get display name (compact natural width by default to save precious terminal columns)
154
204
  let name = key.toLowerCase();
155
205
  if (COMMON_NAMES[name]) {
156
206
  name = COMMON_NAMES[name];
@@ -164,7 +214,14 @@ export function beautifyStatus(
164
214
  name = COMMON_NAMES[name];
165
215
  }
166
216
  }
167
- const visualName = padToVisualWidth(sliceToVisualWidth(name, 10), 10);
217
+ const preset =
218
+ style === "custom"
219
+ ? (customCfg || customStyleConfig || {})
220
+ : (BUILTIN_PRESETS[style] || BUILTIN_PRESETS.stream);
221
+
222
+ const visualName = preset.padName === true
223
+ ? padToVisualWidth(sliceToVisualWidth(name, 10), 10)
224
+ : name;
168
225
 
169
226
  // 2. Extract metrics/progress details safely (e.g. (2) or fraction 2/5 or count)
170
227
  let details: string | undefined;
@@ -234,11 +291,6 @@ export function beautifyStatus(
234
291
  }
235
292
 
236
293
  // 4. Determine state glyph & indicator
237
- const preset =
238
- style === "custom"
239
- ? (customCfg || customStyleConfig || {})
240
- : (BUILTIN_PRESETS[style] || BUILTIN_PRESETS.minimal);
241
-
242
294
  const glyphMap = preset.glyphs || {};
243
295
  let glyph =
244
296
  glyphMap[state] ||
@@ -424,26 +476,66 @@ export function renderBeautifiedFooter(
424
476
  const modelName = ctx?.model?.name || ctx?.model?.id || "";
425
477
  const styledModel = modelName ? (hasThemeFg ? theme.fg("muted", modelName) : modelName) : "";
426
478
 
427
- // 3. Token metrics
428
- let input = 0, output = 0;
429
- if (ctx?.sessionManager?.getBranch) {
479
+ // 3. Token metrics & context usage (full fidelity matching native Pi footer)
480
+ let input = 0, output = 0, cacheRead = 0, cacheWrite = 0, cost = 0;
481
+ let latestCacheHitRate: number | undefined;
482
+
483
+ if (ctx?.sessionManager?.getEntries) {
430
484
  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;
485
+ for (const entry of ctx.sessionManager.getEntries()) {
486
+ if (entry.type === "usage" && entry.usage) {
487
+ input += entry.usage.input || 0;
488
+ output += entry.usage.output || 0;
489
+ cacheRead += entry.usage.cacheRead || 0;
490
+ cacheWrite += entry.usage.cacheWrite || 0;
491
+ cost += entry.usage.cost?.total || 0;
492
+ } else if (entry.type === "message" && entry.message?.role === "assistant" && entry.message?.usage) {
493
+ const u = entry.message.usage;
434
494
  input += u.input || 0;
435
495
  output += u.output || 0;
496
+ cacheRead += u.cacheRead || 0;
497
+ cacheWrite += u.cacheWrite || 0;
498
+ cost += u.cost?.total || 0;
499
+ const promptTokens = (u.input || 0) + (u.cacheRead || 0) + (u.cacheWrite || 0);
500
+ if (promptTokens > 0 && u.cacheRead) {
501
+ latestCacheHitRate = (u.cacheRead / promptTokens) * 100;
502
+ }
436
503
  }
437
504
  }
438
505
  } catch (_) {}
439
506
  }
440
- const fmt = (n: number) => (n < 1000 ? `${n}` : `${(n / 1000).toFixed(1)}k`);
441
- const tokenStr = input > 0 || output > 0 ? `↑${fmt(input)} ↓${fmt(output)}` : "";
507
+
508
+ const fmt = (n: number) => {
509
+ if (n < 1000) return `${n}`;
510
+ if (n < 10000) return `${(n / 1000).toFixed(1)}k`;
511
+ if (n < 1000000) return `${Math.round(n / 1000)}k`;
512
+ if (n < 10000000) return `${(n / 1000000).toFixed(1)}M`;
513
+ return `${Math.round(n / 1000000)}M`;
514
+ };
515
+
516
+ const statParts: string[] = [];
517
+ if (input > 0) statParts.push(`↑${fmt(input)}`);
518
+ if (output > 0) statParts.push(`↓${fmt(output)}`);
519
+ if (cacheRead > 0) statParts.push(`R${fmt(cacheRead)}`);
520
+ if (latestCacheHitRate !== undefined) statParts.push(`CH${latestCacheHitRate.toFixed(1)}%`);
521
+ if (cost > 0) statParts.push(`$${cost.toFixed(3)}`);
522
+
523
+ const contextUsage = ctx?.getContextUsage?.();
524
+ const contextWindow = contextUsage?.contextWindow || ctx?.model?.contextWindow || 0;
525
+ if (contextUsage?.percent !== undefined && contextUsage?.percent !== null) {
526
+ statParts.push(`${contextUsage.percent.toFixed(1)}%/${fmt(contextWindow)} (auto)`);
527
+ }
528
+
529
+ const tokenStr = statParts.join(" ");
442
530
  const styledTokens = tokenStr ? (hasThemeFg ? theme.fg("dim", tokenStr) : tokenStr) : "";
443
531
 
532
+ // Thinking level
533
+ const thinking = ctx?.thinkingLevel && ctx.thinkingLevel !== "off" ? ` • ${ctx.thinkingLevel}` : "";
534
+ const fullModelStr = styledModel ? `${styledModel}${hasThemeFg ? theme.fg("dim", thinking) : thinking}` : "";
535
+
444
536
  // Left & right parts of Line 1
445
537
  const leftText = styledBranch;
446
- const rightParts = [styledTokens, styledModel].filter(Boolean);
538
+ const rightParts = [styledTokens, fullModelStr].filter(Boolean);
447
539
  const rightText = rightParts.join(styledSep);
448
540
 
449
541
  const leftVis = getStringWidth(leftText);
@@ -492,7 +584,7 @@ export default function (pi: ExtensionAPI) {
492
584
  originalStatuses.clear();
493
585
  lastRenderedStatus.clear();
494
586
 
495
- // Hook setStatus for clean status badge formatting (never hijack Pi's native footer)
587
+ // 1. Hook setStatus for clean status badge formatting (always active unless style === "off")
496
588
  const originalSetStatus = ctx.ui.setStatus;
497
589
  if (originalSetStatus && !(originalSetStatus as any).__beautifierHooked) {
498
590
  const wrapped = function (key: string, value: string | undefined) {
@@ -511,6 +603,28 @@ export default function (pi: ExtensionAPI) {
511
603
  (wrapped as any).__beautifierHooked = true;
512
604
  ctx.ui.setStatus = wrapped;
513
605
  }
606
+
607
+ // 2. Hook setFooter ONLY if user explicitly opted in with fullFooter === true!
608
+ // By default fullFooter is FALSE: Pi's native top two lines remain 100% native and untouched!
609
+ if (fullFooterEnabled && currentStyle !== "off" && typeof (ctx.ui as any).setFooter === "function") {
610
+ (ctx.ui as any).setFooter((tui: any, theme: any, footerData: any) => {
611
+ const unsub =
612
+ typeof footerData?.onBranchChange === "function"
613
+ ? footerData.onBranchChange(() => tui.requestRender())
614
+ : undefined;
615
+
616
+ return {
617
+ dispose: unsub,
618
+ invalidate() {},
619
+ render(width: number): string[] {
620
+ if (currentStyle === "off" || !fullFooterEnabled) return [];
621
+ return renderBeautifiedFooter(width, tui, theme, footerData, ctx, currentStyle, customStyleConfig);
622
+ },
623
+ };
624
+ });
625
+ } else if (typeof (ctx.ui as any).setFooter === "function") {
626
+ (ctx.ui as any).setFooter(undefined); // Restore native Pi footer
627
+ }
514
628
  });
515
629
 
516
630
  // Dynamic status bar style controller registry
@@ -522,11 +636,13 @@ export default function (pi: ExtensionAPI) {
522
636
  }
523
637
 
524
638
  const styleOptions = [
525
- "custom - [AI Prompt Recipe] Let your agent generate or tailor your own style",
526
- "minimal - Clean chevron indicator (e.g. name ❯ ●)",
527
- "glass - Framed capsule badge (e.g. ▕ name ● ▏)",
528
- "glow - High-contrast inverse pill (e.g. name [ ● ])",
529
- "off - Pass through raw status output",
639
+ "stream - Clean chevron stream (e.g. name ❯ ● ready)",
640
+ "powerline - Angled brackets & diamonds (e.g. name ‹◆ ready›)",
641
+ "matrix - Dot-matrix brackets & stars (e.g. name ⦗✦ ready⦘)",
642
+ "glow - High-contrast inverse pill (e.g. name [ ● ready ])",
643
+ "custom - [AI Prompt Recipe] Let your agent tailor your bespoke style",
644
+ `footer - Toggle Full-Footer beautification (Git/Model/Tokens) [Currently: ${fullFooterEnabled ? "ON" : "OFF"}]`,
645
+ "off - Revert to native Pi status bar & clean settings",
530
646
  ];
531
647
 
532
648
  const choice = await ctx.ui.select(`Choose status style (Current: ${currentStyle}):`, styleOptions);
@@ -535,24 +651,96 @@ export default function (pi: ExtensionAPI) {
535
651
 
536
652
  const styleKey = choice.split(" ")[0].trim();
537
653
 
654
+ if (styleKey === "footer") {
655
+ fullFooterEnabled = !fullFooterEnabled;
656
+ const { targetSettingsPath } = loadConfig(ctx.cwd || process.cwd());
657
+ try {
658
+ if (fs.existsSync(targetSettingsPath)) {
659
+ const data = fs.readFileSync(targetSettingsPath, "utf-8");
660
+ const config = JSON.parse(data);
661
+ if (!config.beautifier) config.beautifier = {};
662
+ config.beautifier.fullFooter = fullFooterEnabled;
663
+ const tempSettings = `${targetSettingsPath}.tmp.${process.pid}.${Date.now()}`;
664
+ fs.writeFileSync(tempSettings, JSON.stringify(config, null, 2), "utf-8");
665
+ fs.renameSync(tempSettings, targetSettingsPath);
666
+ }
667
+ } catch (_) {}
668
+
669
+ if (fullFooterEnabled && currentStyle !== "off" && typeof (ctx.ui as any).setFooter === "function") {
670
+ (ctx.ui as any).setFooter((tui: any, theme: any, footerData: any) => {
671
+ const unsub = typeof footerData?.onBranchChange === "function" ? footerData.onBranchChange(() => tui.requestRender()) : undefined;
672
+ return {
673
+ dispose: unsub,
674
+ invalidate() {},
675
+ render(width: number): string[] {
676
+ if (currentStyle === "off" || !fullFooterEnabled) return [];
677
+ return renderBeautifiedFooter(width, tui, theme, footerData, ctx, currentStyle, customStyleConfig);
678
+ }
679
+ };
680
+ });
681
+ } else if (typeof (ctx.ui as any).setFooter === "function") {
682
+ (ctx.ui as any).setFooter(undefined);
683
+ }
684
+
685
+ ctx.ui.notify(`Full-footer beautification is now ${fullFooterEnabled ? "ENABLED" : "DISABLED (Native Pi Footer)"}`, "info");
686
+ return;
687
+ }
688
+
689
+ if (styleKey === "off") {
690
+ currentStyle = "off";
691
+ customStyleConfig = undefined;
692
+ lastRenderedStatus.clear();
693
+
694
+ // Restore all raw original statuses immediately
695
+ for (const [key, val] of originalStatuses.entries()) {
696
+ ctx.ui.setStatus(key, val);
697
+ }
698
+
699
+ // Zero-trace physical cleanup: completely prune the `beautifier` key from settings.json!
700
+ const { targetSettingsPath } = loadConfig(ctx.cwd || process.cwd());
701
+ try {
702
+ if (fs.existsSync(targetSettingsPath)) {
703
+ const data = fs.readFileSync(targetSettingsPath, "utf-8");
704
+ const config = JSON.parse(data);
705
+ if (config.beautifier) {
706
+ delete config.beautifier;
707
+ const tempSettings = `${targetSettingsPath}.tmp.${process.pid}.${Date.now()}`;
708
+ fs.writeFileSync(tempSettings, JSON.stringify(config, null, 2), "utf-8");
709
+ fs.renameSync(tempSettings, targetSettingsPath);
710
+ }
711
+ }
712
+ } catch (_e) {
713
+ // Silently ignore disk write errors
714
+ }
715
+
716
+ ctx.ui.notify("Reverted to native Pi status bar. Cleaned up settings.json.", "info");
717
+ return;
718
+ }
719
+
538
720
  if (styleKey === "custom") {
539
721
  const promptRecipe = [
540
722
  "You are acting as my Pi TUI Status Bar designer for `pi-tui-status-beautifier`.",
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.",
542
- "2. Once I select or describe my aesthetic, save the configuration directly into `./.pi/settings.json` (or `~/.pi/agent/settings.json`) under `beautifier`:",
723
+ "1. In my primary language, call `ask_user_question` with ASCII visual preview mockups showing ONLY realistic extension statuses (e.g. `chrome`, `toolflow`, `plan`):",
724
+ " - Powerline: chrome ‹◆ ready› | toolflow ‹◈ 2/5›",
725
+ " - Matrix: chrome ⦗✦ ready⦘ | toolflow ⦗◍ 2/5⦘",
726
+ " - Stream: chrome ❯ ● ready | toolflow ❯ ◐ 2/5",
727
+ " - Glow: chrome [ ● ready ] | toolflow [ ◐ 2/5 ]",
728
+ " (Do NOT include git branch or model name in mockups).",
729
+ "2. If I describe or pick a style, write the configuration into `./.pi/settings.json` (or `~/.pi/agent/settings.json`) under `beautifier`:",
543
730
  " {",
544
731
  ' "beautifier": {',
545
732
  ' "style": "custom",',
546
733
  ' "custom": {',
547
734
  ' "separator": " » ",',
548
735
  ' "brackets": ["«", "»"],',
549
- ' "prefix": "✦ ",',
736
+ ' "prefix": "",',
550
737
  ' "suffix": "",',
551
738
  ' "glyphs": { "success": "✦", "warning": "◇", "error": "✖", "accent": "✧" }',
552
739
  " }",
553
740
  " }",
554
741
  " }",
555
- "Do NOT crawl filesystem or edit extension source code files."
742
+ "3. If I say revert, restore, default, off, or cancel, DELETE the `beautifier` key from `settings.json` and exit quietly without asking more questions.",
743
+ "4. Do NOT search filesystem or edit extension source files."
556
744
  ].join("\n");
557
745
 
558
746
  if (typeof (ctx.ui as any).setEditorText === "function") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-tui-status-beautifier",
3
- "version": "1.3.2",
3
+ "version": "1.5.0",
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": [