@lokiyou/pi-nano-footer 0.2.0 → 0.3.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 +50 -128
- package/package.json +2 -2
package/index.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
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
7
|
* 当模型工作时(processing):
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
8
|
+
* - 输入框边框呈呼吸脉冲发光效果(霓虹色循环)
|
|
9
|
+
* - "Working..." 保持原样
|
|
10
|
+
* - Footer 不变
|
|
11
11
|
*
|
|
12
12
|
* 安装:放到 ~/.pi/agent/extensions/ 后重启 pi 即可自动加载。
|
|
13
13
|
* 临时测试:pi -e ~/.pi/agent/extensions/pi-nano-footer.ts
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
17
17
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { Editor } from "@earendil-works/pi-tui";
|
|
18
19
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
19
20
|
|
|
20
21
|
// ── Nerd Font 图标(与 pi-powerline-footer 完全一致) ──
|
|
@@ -48,16 +49,13 @@ const C = {
|
|
|
48
49
|
border: "#dfe6e9",
|
|
49
50
|
};
|
|
50
51
|
|
|
51
|
-
// ──
|
|
52
|
-
const
|
|
52
|
+
// ── 脉冲发光色轮(5色循环,更柔和的呼吸感) ──
|
|
53
|
+
const GLOW_PALETTE = [
|
|
53
54
|
"#ff3cac", // 热粉
|
|
54
|
-
"#ff6b9d", // 粉红
|
|
55
55
|
"#ff9f1c", // 橙
|
|
56
|
-
"#fdcb6e", // 黄
|
|
57
56
|
"#00ff87", // 荧光绿
|
|
58
57
|
"#00d4ff", // 青蓝
|
|
59
58
|
"#a855f7", // 紫
|
|
60
|
-
"#6c5ce7", // 紫蓝
|
|
61
59
|
];
|
|
62
60
|
|
|
63
61
|
// ── Rainbow 色表(high / xhigh 思考等级用) ──
|
|
@@ -66,63 +64,55 @@ const RAINBOW = [
|
|
|
66
64
|
"#89d281", "#00afaf", "#178fb9", "#b281d6",
|
|
67
65
|
];
|
|
68
66
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
let pulsePhase = 0; // 脉冲动画相位
|
|
73
|
-
let requestRender: (() => void) | undefined;
|
|
74
|
-
const refresh = () => requestRender?.();
|
|
67
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
68
|
+
// 输入框脉冲发光(Editor.borderColor monkey-patch)
|
|
69
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
75
70
|
|
|
76
|
-
|
|
77
|
-
const PULSE_INTERVAL_MS = 100;
|
|
78
|
-
let pulseTimer: ReturnType<typeof setInterval> | null = null;
|
|
71
|
+
let working = false;
|
|
79
72
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
rt();
|
|
85
|
-
}, PULSE_INTERVAL_MS);
|
|
86
|
-
}
|
|
73
|
+
/** 时间相位:每 120ms 进一帧 */
|
|
74
|
+
function getPhase(): number {
|
|
75
|
+
return Math.floor(performance.now() / 120);
|
|
76
|
+
}
|
|
87
77
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
78
|
+
/** 保存原始 render */
|
|
79
|
+
const origRender = Editor.prototype.render;
|
|
80
|
+
|
|
81
|
+
Editor.prototype.render = function (width: number): string[] {
|
|
82
|
+
if (working) {
|
|
83
|
+
const saved = this.borderColor; // 保存原始边框颜色函数
|
|
84
|
+
const phase = getPhase() % GLOW_PALETTE.length;
|
|
85
|
+
const hex = GLOW_PALETTE[phase];
|
|
86
|
+
// 临时用脉冲色渲染边框
|
|
87
|
+
this.borderColor = (str: string) => ansi(hex, str);
|
|
88
|
+
try {
|
|
89
|
+
return origRender.call(this, width);
|
|
90
|
+
} finally {
|
|
91
|
+
this.borderColor = saved; // 恢复原始颜色
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
|
+
return origRender.call(this, width);
|
|
95
|
+
};
|
|
94
96
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
98
|
+
// Extension
|
|
99
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
97
100
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
export default function (pi: ExtensionAPI) {
|
|
102
|
+
let thinkingLevel = "off";
|
|
103
|
+
let requestRender: (() => void) | undefined;
|
|
104
|
+
const refresh = () => requestRender?.();
|
|
101
105
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
invalidate() {},
|
|
105
|
-
render(width: number): string[] {
|
|
106
|
-
if (!working || width <= 0) return [];
|
|
107
|
-
return [renderGlowLine(width, pulsePhase)];
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
|
-
}, { placement: "aboveEditor" });
|
|
106
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
107
|
+
thinkingLevel = pi.getThinkingLevel();
|
|
111
108
|
|
|
112
|
-
// ── Footer ──
|
|
113
109
|
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
114
110
|
requestRender = () => tui.requestRender();
|
|
115
|
-
startPulse(() => tui.requestRender());
|
|
116
|
-
|
|
117
111
|
const unsubBranch = footerData.onBranchChange(() => tui.requestRender());
|
|
118
112
|
const clock = setInterval(() => tui.requestRender(), 30_000);
|
|
119
113
|
|
|
120
114
|
return {
|
|
121
|
-
dispose() {
|
|
122
|
-
unsubBranch();
|
|
123
|
-
clearInterval(clock);
|
|
124
|
-
stopPulse();
|
|
125
|
-
},
|
|
115
|
+
dispose() { unsubBranch(); clearInterval(clock); },
|
|
126
116
|
invalidate() {},
|
|
127
117
|
render(width: number): string[] {
|
|
128
118
|
if (width <= 0) return [];
|
|
@@ -134,20 +124,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
134
124
|
// 1. 模型 —— #ff3cac 热粉色
|
|
135
125
|
parts.push(ansi(C.model, `${icons.model} ${shorten(ctx.model?.id ?? "–")}`));
|
|
136
126
|
|
|
137
|
-
// 2.
|
|
138
|
-
|
|
139
|
-
if (working) {
|
|
140
|
-
parts.push(renderPulseDot(pulsePhase));
|
|
141
|
-
} else {
|
|
142
|
-
parts.push(renderThinkingLevel(thinkingLevel));
|
|
143
|
-
}
|
|
127
|
+
// 2. 思考等级 —— 按等级变色,high/xhigh 彩虹
|
|
128
|
+
parts.push(renderThinkingN(thinkingLevel));
|
|
144
129
|
|
|
145
130
|
// 3. 目录 basename —— #00d4ff 青蓝色
|
|
146
131
|
const dir = ctx.cwd.replace(/\\/g, "/").split("/").filter(Boolean).pop() || ctx.cwd;
|
|
147
132
|
parts.push(ansi(C.path, `${icons.folder} ${dir}`));
|
|
148
133
|
|
|
149
134
|
// 4. 上下文用量 —— #6c5ce7 紫 / #fdcb6e 黄 / #ff3366 红
|
|
150
|
-
parts.push(
|
|
135
|
+
parts.push(renderContextN(ctx));
|
|
151
136
|
|
|
152
137
|
// 5. Token 用量 —— #00ff87 荧光绿
|
|
153
138
|
const { input, cost } = calcTotals(ctx);
|
|
@@ -170,10 +155,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
170
155
|
});
|
|
171
156
|
pi.on("model_select", () => refresh());
|
|
172
157
|
|
|
173
|
-
// 工作状态切换
|
|
158
|
+
// 工作状态切换 → 控制脉冲发光
|
|
174
159
|
pi.on("agent_start", () => {
|
|
175
160
|
working = true;
|
|
176
|
-
pulsePhase = 0;
|
|
177
161
|
refresh();
|
|
178
162
|
});
|
|
179
163
|
pi.on("agent_end", () => {
|
|
@@ -181,20 +165,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
181
165
|
refresh();
|
|
182
166
|
});
|
|
183
167
|
|
|
184
|
-
// 传统事件:保持 footer 刷新
|
|
185
168
|
pi.on("turn_start", () => refresh());
|
|
186
169
|
pi.on("turn_end", () => refresh());
|
|
187
170
|
|
|
188
171
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
189
172
|
ctx.ui.setFooter(undefined);
|
|
190
|
-
ctx.ui.setWidget("nano-glow", undefined);
|
|
191
|
-
stopPulse();
|
|
192
173
|
requestRender = undefined;
|
|
193
174
|
});
|
|
194
175
|
}
|
|
195
176
|
|
|
196
177
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
197
|
-
//
|
|
178
|
+
// Helper 函数
|
|
198
179
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
199
180
|
|
|
200
181
|
/** 16 进制色值 → ANSI 24-bit true color 前景色转义序列 */
|
|
@@ -206,62 +187,8 @@ function ansi(hex: string, text: string): string {
|
|
|
206
187
|
return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
|
|
207
188
|
}
|
|
208
189
|
|
|
209
|
-
/**
|
|
210
|
-
function
|
|
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 {
|
|
190
|
+
/** 渲染思考等级 */
|
|
191
|
+
function renderThinkingN(level: string): string {
|
|
265
192
|
const label = level === "minimal" ? "min"
|
|
266
193
|
: level === "medium" ? "med"
|
|
267
194
|
: level;
|
|
@@ -282,9 +209,8 @@ function renderThinkingLevel(level: string): string {
|
|
|
282
209
|
}
|
|
283
210
|
}
|
|
284
211
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
function renderContextBar(ctx: any): string {
|
|
212
|
+
/** 渲染上下文用量,带百分比阈值颜色 */
|
|
213
|
+
function renderContextN(ctx: any): string {
|
|
288
214
|
const usage = ctx.getContextUsage();
|
|
289
215
|
const pct = usage?.percent;
|
|
290
216
|
const color = pct == null ? C.context
|
|
@@ -297,10 +223,6 @@ function renderContextBar(ctx: any): string {
|
|
|
297
223
|
return ansi(color, `${icons.context} ${pct?.toFixed(1) ?? "?"}%${maxStr}`);
|
|
298
224
|
}
|
|
299
225
|
|
|
300
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
301
|
-
// 工具函数
|
|
302
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
303
|
-
|
|
304
226
|
/** 遍历会话分支,累计 input token 数和总费用 */
|
|
305
227
|
function calcTotals(ctx: any): { input: number; cost: number } {
|
|
306
228
|
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.3.0",
|
|
4
|
+
"description": "超轻量 powerline 风格 footer for Pi Coding Agent — 输入框脉冲发光,霓虹配色,实时显示模型、目录、上下文、token 和费用",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"pi-package",
|