@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.
Files changed (2) hide show
  1. package/index.ts +50 -128
  2. 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
- * - Footer 中显示脉冲指示点 ● 代替思考等级文字
10
- * - 没有任何 "working..." 文字
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 PULSE_PALETTE = [
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
- export default function (pi: ExtensionAPI) {
70
- let thinkingLevel = "off";
71
- let working = false; // 模型是否正在工作
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
- 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
- }
73
+ /** 时间相位:每 120ms 进一帧 */
74
+ function getPhase(): number {
75
+ return Math.floor(performance.now() / 120);
76
+ }
87
77
 
88
- function stopPulse() {
89
- if (pulseTimer) {
90
- clearInterval(pulseTimer);
91
- pulseTimer = null;
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
- pi.on("session_start", async (_event, ctx) => {
96
- thinkingLevel = pi.getThinkingLevel();
97
+ // ═══════════════════════════════════════════════════════════════════════════
98
+ // Extension
99
+ // ═══════════════════════════════════════════════════════════════════════════
97
100
 
98
- // ── 脉冲发光 Widget(输入框上方) ──
99
- ctx.ui.setWidget("nano-glow", (tui) => {
100
- startPulse(() => tui.requestRender());
101
+ export default function (pi: ExtensionAPI) {
102
+ let thinkingLevel = "off";
103
+ let requestRender: (() => void) | undefined;
104
+ const refresh = () => requestRender?.();
101
105
 
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" });
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(renderContextBar(ctx));
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
- /** 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 {
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.2.0",
4
- "description": "超轻量 powerline 风格 footer for Pi Coding Agent — 霓虹配色,脉冲发光指示,实时显示模型、目录、上下文、token 和费用",
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",