@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.
Files changed (2) hide show
  1. package/index.ts +150 -13
  2. 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() { unsubBranch(); clearInterval(clock); },
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. 思考等级 —— 按等级变色,high/xhigh 彩虹
79
- parts.push(renderThinkingN(thinkingLevel));
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(renderContextN(ctx));
150
+ parts.push(renderContextBar(ctx));
87
151
 
88
- // 5. Cache read —— #00ff87 荧光绿
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
- // ── 事件订阅:保持 footer 实时刷新 ──
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
- // Helper 函数
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 renderThinkingN(level: string): string {
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
- function renderContextN(ctx: any): string {
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.1.0",
4
- "description": "超轻量 powerline 风格 footer for Pi Coding Agent — 霓虹配色,实时显示模型、思考等级、目录、上下文、token 和费用",
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",