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.
- package/extensions/tui-status-beautifier.ts +204 -34
- package/package.json +1 -1
|
@@ -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
|
-
/[
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
|
384
|
-
"
|
|
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(
|
|
403
|
-
const data = fs.readFileSync(
|
|
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 = `${
|
|
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,
|
|
582
|
+
fs.renameSync(tempSettings, targetSettingsPath);
|
|
413
583
|
}
|
|
414
584
|
} catch (_e) {
|
|
415
585
|
// Silently ignore disk write errors
|
package/package.json
CHANGED