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.
- package/extensions/tui-status-beautifier.ts +215 -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
|
|
|
@@ -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
|
|
384
|
-
"
|
|
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(
|
|
403
|
-
const data = fs.readFileSync(
|
|
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 = `${
|
|
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,
|
|
593
|
+
fs.renameSync(tempSettings, targetSettingsPath);
|
|
413
594
|
}
|
|
414
595
|
} catch (_e) {
|
|
415
596
|
// Silently ignore disk write errors
|
package/package.json
CHANGED