@lokiyou/pi-nano-footer 0.1.0 → 0.2.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.
- package/index.ts +150 -13
- package/package.json +2 -2
package/index.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pi-nano-footer — 超轻量 powerline 风格 footer
|
|
2
|
+
* pi-nano-footer — 超轻量 powerline 风格 footer + 脉冲发光效果
|
|
3
3
|
*
|
|
4
4
|
* 精确复刻 pi-powerline-footer default 预设的样式 +
|
|
5
5
|
* 用户自定义霓虹色配色方案。
|
|
6
6
|
*
|
|
7
|
+
* 当模型工作时(processing):
|
|
8
|
+
* - 输入框上方显示一条脉冲发光带(霓虹色循环)
|
|
9
|
+
* - Footer 中显示脉冲指示点 ● 代替思考等级文字
|
|
10
|
+
* - 没有任何 "working..." 文字
|
|
11
|
+
*
|
|
7
12
|
* 安装:放到 ~/.pi/agent/extensions/ 后重启 pi 即可自动加载。
|
|
8
13
|
* 临时测试:pi -e ~/.pi/agent/extensions/pi-nano-footer.ts
|
|
9
14
|
*/
|
|
@@ -43,6 +48,18 @@ const C = {
|
|
|
43
48
|
border: "#dfe6e9",
|
|
44
49
|
};
|
|
45
50
|
|
|
51
|
+
// ── 脉冲动画颜色轮盘(霓虹色系) ──
|
|
52
|
+
const PULSE_PALETTE = [
|
|
53
|
+
"#ff3cac", // 热粉
|
|
54
|
+
"#ff6b9d", // 粉红
|
|
55
|
+
"#ff9f1c", // 橙
|
|
56
|
+
"#fdcb6e", // 黄
|
|
57
|
+
"#00ff87", // 荧光绿
|
|
58
|
+
"#00d4ff", // 青蓝
|
|
59
|
+
"#a855f7", // 紫
|
|
60
|
+
"#6c5ce7", // 紫蓝
|
|
61
|
+
];
|
|
62
|
+
|
|
46
63
|
// ── Rainbow 色表(high / xhigh 思考等级用) ──
|
|
47
64
|
const RAINBOW = [
|
|
48
65
|
"#b281d6", "#d787af", "#febc38", "#e4c00f",
|
|
@@ -51,19 +68,61 @@ const RAINBOW = [
|
|
|
51
68
|
|
|
52
69
|
export default function (pi: ExtensionAPI) {
|
|
53
70
|
let thinkingLevel = "off";
|
|
71
|
+
let working = false; // 模型是否正在工作
|
|
72
|
+
let pulsePhase = 0; // 脉冲动画相位
|
|
54
73
|
let requestRender: (() => void) | undefined;
|
|
55
74
|
const refresh = () => requestRender?.();
|
|
56
75
|
|
|
76
|
+
// ── 脉冲动画定时器 ──
|
|
77
|
+
const PULSE_INTERVAL_MS = 100;
|
|
78
|
+
let pulseTimer: ReturnType<typeof setInterval> | null = null;
|
|
79
|
+
|
|
80
|
+
function startPulse(rt: () => void) {
|
|
81
|
+
if (pulseTimer) return;
|
|
82
|
+
pulseTimer = setInterval(() => {
|
|
83
|
+
pulsePhase = (pulsePhase + 1) % PULSE_PALETTE.length;
|
|
84
|
+
rt();
|
|
85
|
+
}, PULSE_INTERVAL_MS);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function stopPulse() {
|
|
89
|
+
if (pulseTimer) {
|
|
90
|
+
clearInterval(pulseTimer);
|
|
91
|
+
pulseTimer = null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
57
95
|
pi.on("session_start", async (_event, ctx) => {
|
|
58
96
|
thinkingLevel = pi.getThinkingLevel();
|
|
59
97
|
|
|
98
|
+
// ── 脉冲发光 Widget(输入框上方) ──
|
|
99
|
+
ctx.ui.setWidget("nano-glow", (tui) => {
|
|
100
|
+
startPulse(() => tui.requestRender());
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
dispose() { stopPulse(); },
|
|
104
|
+
invalidate() {},
|
|
105
|
+
render(width: number): string[] {
|
|
106
|
+
if (!working || width <= 0) return [];
|
|
107
|
+
return [renderGlowLine(width, pulsePhase)];
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}, { placement: "aboveEditor" });
|
|
111
|
+
|
|
112
|
+
// ── Footer ──
|
|
60
113
|
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
61
114
|
requestRender = () => tui.requestRender();
|
|
115
|
+
startPulse(() => tui.requestRender());
|
|
116
|
+
|
|
62
117
|
const unsubBranch = footerData.onBranchChange(() => tui.requestRender());
|
|
63
118
|
const clock = setInterval(() => tui.requestRender(), 30_000);
|
|
64
119
|
|
|
65
120
|
return {
|
|
66
|
-
dispose() {
|
|
121
|
+
dispose() {
|
|
122
|
+
unsubBranch();
|
|
123
|
+
clearInterval(clock);
|
|
124
|
+
stopPulse();
|
|
125
|
+
},
|
|
67
126
|
invalidate() {},
|
|
68
127
|
render(width: number): string[] {
|
|
69
128
|
if (width <= 0) return [];
|
|
@@ -75,17 +134,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
75
134
|
// 1. 模型 —— #ff3cac 热粉色
|
|
76
135
|
parts.push(ansi(C.model, `${icons.model} ${shorten(ctx.model?.id ?? "–")}`));
|
|
77
136
|
|
|
78
|
-
// 2.
|
|
79
|
-
|
|
137
|
+
// 2. 工作时 → 脉冲指示点,不显示任何文字
|
|
138
|
+
// 空闲时 → 显示思考等级
|
|
139
|
+
if (working) {
|
|
140
|
+
parts.push(renderPulseDot(pulsePhase));
|
|
141
|
+
} else {
|
|
142
|
+
parts.push(renderThinkingLevel(thinkingLevel));
|
|
143
|
+
}
|
|
80
144
|
|
|
81
145
|
// 3. 目录 basename —— #00d4ff 青蓝色
|
|
82
146
|
const dir = ctx.cwd.replace(/\\/g, "/").split("/").filter(Boolean).pop() || ctx.cwd;
|
|
83
147
|
parts.push(ansi(C.path, `${icons.folder} ${dir}`));
|
|
84
148
|
|
|
85
149
|
// 4. 上下文用量 —— #6c5ce7 紫 / #fdcb6e 黄 / #ff3366 红
|
|
86
|
-
parts.push(
|
|
150
|
+
parts.push(renderContextBar(ctx));
|
|
87
151
|
|
|
88
|
-
// 5.
|
|
152
|
+
// 5. Token 用量 —— #00ff87 荧光绿
|
|
89
153
|
const { input, cost } = calcTotals(ctx);
|
|
90
154
|
parts.push(ansi(C.tokens, `${icons.cache} ${icons.input} ${fmt(input)}`));
|
|
91
155
|
|
|
@@ -98,25 +162,39 @@ export default function (pi: ExtensionAPI) {
|
|
|
98
162
|
});
|
|
99
163
|
});
|
|
100
164
|
|
|
101
|
-
// ──
|
|
165
|
+
// ── 事件订阅 ──
|
|
102
166
|
|
|
103
167
|
pi.on("thinking_level_select", (event) => {
|
|
104
168
|
thinkingLevel = event.level;
|
|
105
169
|
refresh();
|
|
106
170
|
});
|
|
107
171
|
pi.on("model_select", () => refresh());
|
|
172
|
+
|
|
173
|
+
// 工作状态切换
|
|
174
|
+
pi.on("agent_start", () => {
|
|
175
|
+
working = true;
|
|
176
|
+
pulsePhase = 0;
|
|
177
|
+
refresh();
|
|
178
|
+
});
|
|
179
|
+
pi.on("agent_end", () => {
|
|
180
|
+
working = false;
|
|
181
|
+
refresh();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// 传统事件:保持 footer 刷新
|
|
108
185
|
pi.on("turn_start", () => refresh());
|
|
109
186
|
pi.on("turn_end", () => refresh());
|
|
110
|
-
pi.on("agent_end", () => refresh());
|
|
111
187
|
|
|
112
188
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
113
189
|
ctx.ui.setFooter(undefined);
|
|
190
|
+
ctx.ui.setWidget("nano-glow", undefined);
|
|
191
|
+
stopPulse();
|
|
114
192
|
requestRender = undefined;
|
|
115
193
|
});
|
|
116
194
|
}
|
|
117
195
|
|
|
118
196
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
119
|
-
//
|
|
197
|
+
// 渲染函数
|
|
120
198
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
121
199
|
|
|
122
200
|
/** 16 进制色值 → ANSI 24-bit true color 前景色转义序列 */
|
|
@@ -128,8 +206,62 @@ function ansi(hex: string, text: string): string {
|
|
|
128
206
|
return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
|
|
129
207
|
}
|
|
130
208
|
|
|
131
|
-
/**
|
|
132
|
-
function
|
|
209
|
+
/** 16 进制色值 → ANSI 24-bit true color 背景色转义序列 */
|
|
210
|
+
function ansiBg(hex: string, text: string): string {
|
|
211
|
+
const h = hex.replace("#", "");
|
|
212
|
+
const r = parseInt(h.slice(0, 2), 16);
|
|
213
|
+
const g = parseInt(h.slice(2, 4), 16);
|
|
214
|
+
const b = parseInt(h.slice(4, 6), 16);
|
|
215
|
+
return `\x1b[48;2;${r};${g};${b}m${text}\x1b[0m`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** 基于亮度的半透明色(在黑色背景上混合) */
|
|
219
|
+
function alphaOnBlack(hex: string, alpha: number): string {
|
|
220
|
+
const h = hex.replace("#", "");
|
|
221
|
+
const r = Math.round(parseInt(h.slice(0, 2), 16) * alpha);
|
|
222
|
+
const g = Math.round(parseInt(h.slice(2, 4), 16) * alpha);
|
|
223
|
+
const b = Math.round(parseInt(h.slice(4, 6), 16) * alpha);
|
|
224
|
+
return `\x1b[48;2;${r};${g};${b}m \x1b[0m`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── 脉冲发光带(输入框上方) ──
|
|
228
|
+
|
|
229
|
+
/** 渲染一条脉冲发光带,霓虹色从左到右波浪式前进 */
|
|
230
|
+
function renderGlowLine(width: number, phase: number): string {
|
|
231
|
+
const barWidth = Math.min(width, 60);
|
|
232
|
+
const offset = Math.floor((width - barWidth) / 2);
|
|
233
|
+
let line = "";
|
|
234
|
+
|
|
235
|
+
for (let i = 0; i < width; i++) {
|
|
236
|
+
if (i < offset || i >= offset + barWidth) {
|
|
237
|
+
line += " ";
|
|
238
|
+
} else {
|
|
239
|
+
const pos = (i - offset) / barWidth; // 0..1 位置
|
|
240
|
+
const colorIdx = (phase + Math.floor(pos * 4)) % PULSE_PALETTE.length;
|
|
241
|
+
// 边缘暗、中间亮 → 制作光晕效果
|
|
242
|
+
const edge = Math.sin(pos * Math.PI); // 0→1→0 正弦波
|
|
243
|
+
const alpha = 0.3 + 0.7 * edge; // 边缘 0.3,中间 1.0
|
|
244
|
+
const hex = PULSE_PALETTE[colorIdx].replace("#", "");
|
|
245
|
+
const r = Math.round(parseInt(hex.slice(0, 2), 16) * alpha);
|
|
246
|
+
const g = Math.round(parseInt(hex.slice(2, 4), 16) * alpha);
|
|
247
|
+
const b = Math.round(parseInt(hex.slice(4, 6), 16) * alpha);
|
|
248
|
+
line += `\x1b[48;2;${r};${g};${b}m \x1b[0m`; // 空格背景色 = 发光像素
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return line;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── 脉冲指示点(Footer 中替代思考等级文字) ──
|
|
255
|
+
|
|
256
|
+
/** 渲染一个脉冲发光 ● 指示点 */
|
|
257
|
+
function renderPulseDot(phase: number): string {
|
|
258
|
+
const color = PULSE_PALETTE[phase % PULSE_PALETTE.length];
|
|
259
|
+
return ansi(color, "●");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── 思考等级渲染 ──
|
|
263
|
+
|
|
264
|
+
function renderThinkingLevel(level: string): string {
|
|
133
265
|
const label = level === "minimal" ? "min"
|
|
134
266
|
: level === "medium" ? "med"
|
|
135
267
|
: level;
|
|
@@ -150,8 +282,9 @@ function renderThinkingN(level: string): string {
|
|
|
150
282
|
}
|
|
151
283
|
}
|
|
152
284
|
|
|
153
|
-
|
|
154
|
-
|
|
285
|
+
// ── 上下文用量 ──
|
|
286
|
+
|
|
287
|
+
function renderContextBar(ctx: any): string {
|
|
155
288
|
const usage = ctx.getContextUsage();
|
|
156
289
|
const pct = usage?.percent;
|
|
157
290
|
const color = pct == null ? C.context
|
|
@@ -164,6 +297,10 @@ function renderContextN(ctx: any): string {
|
|
|
164
297
|
return ansi(color, `${icons.context} ${pct?.toFixed(1) ?? "?"}%${maxStr}`);
|
|
165
298
|
}
|
|
166
299
|
|
|
300
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
301
|
+
// 工具函数
|
|
302
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
303
|
+
|
|
167
304
|
/** 遍历会话分支,累计 input token 数和总费用 */
|
|
168
305
|
function calcTotals(ctx: any): { input: number; cost: number } {
|
|
169
306
|
let input = 0, cost = 0;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lokiyou/pi-nano-footer",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "超轻量 powerline 风格 footer for Pi Coding Agent —
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "超轻量 powerline 风格 footer for Pi Coding Agent — 霓虹配色,脉冲发光指示,实时显示模型、目录、上下文、token 和费用",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"pi-package",
|