@lokiyou/pi-nano-footer 0.11.0 → 0.12.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 +296 -297
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -1,297 +1,296 @@
1
- /**
2
- * pi-nano-footer — 超轻量 powerline 风格 footer + 输入框呼吸发光
3
- *
4
- * 精确复刻 pi-powerline-footer default 预设的样式 +
5
- * 用户自定义霓虹色配色方案。
6
- *
7
- * 当模型工作时:
8
- * - 输入框边框基于当前颜色做呼吸发光(明暗渐变)
9
- * - "Working..." 保持原样
10
- * - Footer 不变
11
- */
12
-
13
- import type { AssistantMessage } from "@earendil-works/pi-ai";
14
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
- import { Editor } from "@earendil-works/pi-tui";
16
- import { truncateToWidth } from "@earendil-works/pi-tui";
17
-
18
- // ── Nerd Font 图标(与 pi-powerline-footer 完全一致) ──
19
- const icons = {
20
- model: "\uec19", // nf-md-chip
21
- folder: "\uf115", // nf-fa-folder_open
22
- context: "\ue70f", // nf-dev-database
23
- cache: "\uf1c0", // nf-fa-database
24
- input: "\uf090", // nf-fa-sign_in
25
- cost: "\uf155", // nf-fa-dollar
26
- sep: "\ue0b1", // powerline-thin
27
- };
28
-
29
- // ── 用户自定义霓虹配色 ──
30
- const C = {
31
- model: "#ff3cac",
32
- shellMode: "#00ff87",
33
- path: "#00d4ff",
34
- gitDirty: "#ff9f1c",
35
- gitClean: "#00ff87",
36
- thinking: "#ff6b6b",
37
- thinkingMinimal: "#a855f7",
38
- thinkingLow: "#00d4ff",
39
- thinkingMedium: "#ff9f1c",
40
- context: "#6c5ce7",
41
- contextWarn: "#fdcb6e",
42
- contextError: "#ff3366",
43
- cost: "#fdcb6e",
44
- tokens: "#00ff87",
45
- separator: "#a855f7",
46
- border: "#dfe6e9",
47
- };
48
-
49
- // ── ANSI 24-bit true color 正则(用于从 borderColor 函数提取颜色) ──
50
- const ANSI_RE = /\x1b\[38;2;(\d+);(\d+);(\d+)m/;
51
-
52
- // ── Rainbow 色表(high / xhigh 思考等级用) ──
53
- const RAINBOW = [
54
- "#b281d6", "#d787af", "#febc38", "#e4c00f",
55
- "#89d281", "#00afaf", "#178fb9", "#b281d6",
56
- ];
57
-
58
- // ═══════════════════════════════════════════════════════════════════════════
59
- // 输入框呼吸发光(基于当前边框颜色做明暗呼吸)
60
- // ═══════════════════════════════════════════════════════════════════════════
61
-
62
- let working = false;
63
- let animInterval: ReturnType<typeof setInterval> | undefined;
64
-
65
- /**
66
- * 从 borderColor 函数采样字符,提取当前 RGB 色值
67
- */
68
- function extractRGB(fn: (str: string) => string): [number, number, number] | null {
69
- const sample = fn("─");
70
- const m = sample.match(ANSI_RE);
71
- if (!m) return null;
72
- return [parseInt(m[1]), parseInt(m[2]), parseInt(m[3])];
73
- }
74
-
75
- /** 保存原始 render */
76
- const origRender = Editor.prototype.render;
77
-
78
- Editor.prototype.render = function (width: number): string[] {
79
- if (working) {
80
- const saved = this.borderColor;
81
- const rgb = extractRGB(saved);
82
- if (rgb) {
83
- let [r, g, b] = rgb;
84
- // 智能工作色:向暖橙 #ff9f1c 偏移 40%
85
- // 这样冷色(紫/青)会变暖,暖色(橙/红)保持,过渡自然不突兀
86
- const blend = 0.4;
87
- r = Math.round(r + (0xff - r) * blend);
88
- g = Math.round(g + (0x9f - g) * blend);
89
- b = Math.round(b + (0x1c - b) * blend);
90
- // 呼吸:周期 ~800ms,60fps
91
- const t = 0.5 + 0.5 * Math.sin(performance.now() / 400);
92
- const bright = 0.25 + 0.75 * t;
93
- const glow = t * t * 0.35;
94
- const nr = Math.round(Math.min(255, r * bright + (255 - r) * glow));
95
- const ng = Math.round(Math.min(255, g * bright + (255 - g) * glow));
96
- const nb = Math.round(Math.min(255, b * bright + (255 - b) * glow));
97
- this.borderColor = (str: string) =>
98
- `\x1b[38;2;${nr};${ng};${nb}m${str}\x1b[0m`;
99
- }
100
- try {
101
- return origRender.call(this, width);
102
- } finally {
103
- this.borderColor = saved;
104
- }
105
- }
106
- return origRender.call(this, width);
107
- };
108
-
109
- // ═══════════════════════════════════════════════════════════════════════════
110
- // Extension
111
- // ═══════════════════════════════════════════════════════════════════════════
112
-
113
- export default function (pi: ExtensionAPI) {
114
- let thinkingLevel = "off";
115
- let requestRender: (() => void) | undefined;
116
- const refresh = () => requestRender?.();
117
-
118
- pi.on("session_start", async (_event, ctx) => {
119
- thinkingLevel = pi.getThinkingLevel();
120
-
121
- // 隐藏内置的 "Working..." 指示器,用输入框呼吸代替
122
- ctx.ui.setWorkingVisible(false);
123
-
124
- ctx.ui.setFooter((tui, theme, footerData) => {
125
- requestRender = () => tui.requestRender();
126
- const unsubBranch = footerData.onBranchChange(() => tui.requestRender());
127
- const clock = setInterval(() => tui.requestRender(), 30_000);
128
-
129
- return {
130
- dispose() { unsubBranch(); clearInterval(clock); },
131
- invalidate() {},
132
- render(width: number): string[] {
133
- if (width <= 0) return [];
134
-
135
- // 分隔符:dim 色(低调,不抢眼)
136
- const S = ` ${theme.fg("dim", icons.sep)} `;
137
- const parts: string[] = [];
138
-
139
- // 1. 模型 —— #ff3cac 热粉色
140
- parts.push(ansi(C.model, `${icons.model} ${shorten(ctx.model?.id ?? "–")}`));
141
-
142
- // 2. 思考等级 —— 按等级变色,high/xhigh 彩虹
143
- parts.push(renderThinkingN(thinkingLevel));
144
-
145
- // 3. 目录 basename —— #00d4ff 青蓝色
146
- const dir = ctx.cwd.replace(/\\/g, "/").split("/").filter(Boolean).pop() || ctx.cwd;
147
- parts.push(ansi(C.path, `${icons.folder} ${dir}`));
148
-
149
- // 4. 上下文用量 —— #6c5ce7 紫 / #fdcb6e 黄 / #ff3366 红
150
- parts.push(renderContextN(ctx));
151
-
152
- // 5. Token 用量 —— #00ff87 荧光绿
153
- const { input, cost } = calcTotals(ctx);
154
- parts.push(ansi(C.tokens, `${icons.cache} ${icons.input} ${fmt(input)}`));
155
-
156
- // 6. 费用 —— #fdcb6e 明黄色
157
- parts.push(ansi(C.cost, `${icons.cost} ${cost.toFixed(2)}`));
158
-
159
- return [truncateToWidth(parts.join(S), width, "")];
160
- },
161
- };
162
- });
163
- });
164
-
165
- // ── 事件订阅 ──
166
-
167
- pi.on("thinking_level_select", (event) => {
168
- thinkingLevel = event.level;
169
- refresh();
170
- });
171
- pi.on("model_select", () => refresh());
172
-
173
- // 工作状态切换 控制呼吸发光
174
- pi.on("agent_start", () => {
175
- working = true;
176
- // 50ms 间隔主动刷新,保证呼吸动画 20fps 流畅
177
- if (requestRender) {
178
- animInterval = setInterval(requestRender, 50);
179
- }
180
- refresh();
181
- });
182
- pi.on("agent_end", () => {
183
- working = false;
184
- if (animInterval) {
185
- clearInterval(animInterval);
186
- animInterval = undefined;
187
- }
188
- refresh();
189
- });
190
-
191
- pi.on("turn_start", () => refresh());
192
- pi.on("turn_end", () => refresh());
193
-
194
- pi.on("session_shutdown", (_event, ctx) => {
195
- ctx.ui.setFooter(undefined);
196
- requestRender = undefined;
197
- if (animInterval) {
198
- clearInterval(animInterval);
199
- animInterval = undefined;
200
- }
201
- });
202
- }
203
-
204
- // ═══════════════════════════════════════════════════════════════════════════
205
- // Helper 函数
206
- // ═══════════════════════════════════════════════════════════════════════════
207
-
208
- /** 16 进制色值 ANSI 24-bit true color 前景色转义序列 */
209
- function ansi(hex: string, text: string): string {
210
- const h = hex.replace("#", "");
211
- const r = parseInt(h.slice(0, 2), 16);
212
- const g = parseInt(h.slice(2, 4), 16);
213
- const b = parseInt(h.slice(4, 6), 16);
214
- return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
215
- }
216
-
217
- /** 渲染思考等级 */
218
- function renderThinkingN(level: string): string {
219
- const label = level === "minimal" ? "min"
220
- : level === "medium" ? "med"
221
- : level;
222
- const text = `think:${label}`;
223
-
224
- switch (level) {
225
- case "high":
226
- case "xhigh":
227
- return rainbow(text);
228
- case "minimal":
229
- return ansi(C.thinkingMinimal, text);
230
- case "low":
231
- return ansi(C.thinkingLow, text);
232
- case "medium":
233
- return ansi(C.thinkingMedium, text);
234
- default:
235
- return ansi(C.thinking, text);
236
- }
237
- }
238
-
239
- /** 渲染上下文用量,带百分比阈值颜色 */
240
- function renderContextN(ctx: any): string {
241
- const usage = ctx.getContextUsage();
242
- const pct = usage?.percent;
243
- const color = pct == null ? C.context
244
- : pct >= 90 ? C.contextError
245
- : pct >= 70 ? C.contextWarn
246
- : C.context;
247
- const maxStr = ctx.model?.contextWindow
248
- ? `/${(ctx.model.contextWindow / 1_000_000).toFixed(1)}M`
249
- : "";
250
- return ansi(color, `${icons.context} ${pct?.toFixed(1) ?? "?"}%${maxStr}`);
251
- }
252
-
253
- /** 遍历会话分支,累计 input token 数和总费用 */
254
- function calcTotals(ctx: any): { input: number; cost: number } {
255
- let input = 0, cost = 0;
256
- for (const e of ctx.sessionManager.getBranch()) {
257
- if (e.type === "message" && e.message.role === "assistant") {
258
- const m = e.message as AssistantMessage;
259
- input += m.usage.input ?? 0;
260
- cost += m.usage.cost?.total ?? 0;
261
- }
262
- }
263
- return { input, cost };
264
- }
265
-
266
- /** 格式化数字:1.2k / 45M */
267
- function fmt(n: number): string {
268
- if (n < 1000) return `${n}`;
269
- if (n < 1_000_000) return `${(n / 1000).toFixed(n < 10_000 ? 1 : 0)}k`;
270
- return `${(n / 1_000_000).toFixed(1)}m`;
271
- }
272
-
273
- /** 缩短模型名 */
274
- function shorten(m: string): string {
275
- return m
276
- .replace(/^claude-/i, "")
277
- .replace(/^gpt-/i, "gpt ")
278
- .replace(/-20\d{6}$/, "")
279
- .replace(/-latest$/i, "");
280
- }
281
-
282
- /** 彩虹渐变 ANSI 渲染 */
283
- function rainbow(text: string): string {
284
- let out = "", ci = 0;
285
- for (const ch of text) {
286
- if (ch === " " || ch === ":") {
287
- out += ch;
288
- } else {
289
- const hex = RAINBOW[ci++ % RAINBOW.length].replace("#", "");
290
- const r = parseInt(hex.slice(0, 2), 16);
291
- const g = parseInt(hex.slice(2, 4), 16);
292
- const b = parseInt(hex.slice(4, 6), 16);
293
- out += `\x1b[38;2;${r};${g};${b}m${ch}\x1b[0m`;
294
- }
295
- }
296
- return out;
297
- }
1
+ /**
2
+ * pi-nano-footer — 超轻量 powerline 风格 footer + 输入框呼吸发光
3
+ *
4
+ * 精确复刻 pi-powerline-footer default 预设的样式 +
5
+ * 用户自定义霓虹色配色方案。
6
+ *
7
+ * 当模型工作时:
8
+ * - 输入框边框基于当前颜色做呼吸发光(明暗渐变)
9
+ * - "Working..." 保持原样
10
+ * - Footer 不变
11
+ */
12
+
13
+ import type { AssistantMessage } from "@earendil-works/pi-ai";
14
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
+ import { Editor } from "@earendil-works/pi-tui";
16
+ import { truncateToWidth } from "@earendil-works/pi-tui";
17
+
18
+ // ── Nerd Font 图标(与 pi-powerline-footer 完全一致) ──
19
+ const icons = {
20
+ model: "\uec19", // nf-md-chip
21
+ folder: "\uf115", // nf-fa-folder_open
22
+ context: "\ue70f", // nf-dev-database
23
+ cache: "\uf1c0", // nf-fa-database
24
+ input: "\uf090", // nf-fa-sign_in
25
+ cost: "\uf155", // nf-fa-dollar
26
+ sep: "\ue0b1", // powerline-thin
27
+ };
28
+
29
+ // ── 用户自定义霓虹配色 ──
30
+ const C = {
31
+ model: "#ff3cac",
32
+ shellMode: "#00ff87",
33
+ path: "#00d4ff",
34
+ gitDirty: "#ff9f1c",
35
+ gitClean: "#00ff87",
36
+ thinking: "#ff6b6b",
37
+ thinkingMinimal: "#a855f7",
38
+ thinkingLow: "#00d4ff",
39
+ thinkingMedium: "#ff9f1c",
40
+ context: "#6c5ce7",
41
+ contextWarn: "#fdcb6e",
42
+ contextError: "#ff3366",
43
+ cost: "#fdcb6e",
44
+ tokens: "#00ff87",
45
+ separator: "#a855f7",
46
+ border: "#dfe6e9",
47
+ };
48
+
49
+ // ── ANSI 24-bit true color 正则(用于从 borderColor 函数提取颜色) ──
50
+ const ANSI_RE = /\x1b\[38;2;(\d+);(\d+);(\d+)m/;
51
+
52
+ // ── Rainbow 色表(high / xhigh 思考等级用) ──
53
+ const RAINBOW = [
54
+ "#b281d6", "#d787af", "#febc38", "#e4c00f",
55
+ "#89d281", "#00afaf", "#178fb9", "#b281d6",
56
+ ];
57
+
58
+ // ═══════════════════════════════════════════════════════════════════════════
59
+ // 输入框呼吸发光(基于当前边框颜色做明暗呼吸)
60
+ // ═══════════════════════════════════════════════════════════════════════════
61
+
62
+ let working = false;
63
+ let animInterval: ReturnType<typeof setInterval> | undefined;
64
+
65
+ /**
66
+ * 从 borderColor 函数采样字符,提取当前 RGB 色值
67
+ */
68
+ function extractRGB(fn: (str: string) => string): [number, number, number] | null {
69
+ const sample = fn("─");
70
+ const m = sample.match(ANSI_RE);
71
+ if (!m) return null;
72
+ return [parseInt(m[1]), parseInt(m[2]), parseInt(m[3])];
73
+ }
74
+
75
+ /** 保存原始 render */
76
+ const origRender = Editor.prototype.render;
77
+
78
+ Editor.prototype.render = function (width: number): string[] {
79
+ if (working) {
80
+ const saved = this.borderColor;
81
+ const rgb = extractRGB(saved);
82
+ if (rgb) {
83
+ const [r, g, b] = rgb;
84
+ // 沿用输入框当前颜色,峰值泛白 + 背景阴影辉光
85
+ // 周期 ~800ms,60fps 驱动
86
+ const t = 0.5 + 0.5 * Math.sin(performance.now() / 400);
87
+ const bright = 0.25 + 0.75 * t;
88
+ const glow = t * t * 0.65; // 0~0.65 白色 blend,峰值明显泛白
89
+ const nr = Math.round(Math.min(255, r * bright + (255 - r) * glow));
90
+ const ng = Math.round(Math.min(255, g * bright + (255 - g) * glow));
91
+ const nb = Math.round(Math.min(255, b * bright + (255 - b) * glow));
92
+ // 阴影光晕背景:铺满格子模拟辉光
93
+ const sr = Math.round(nr * 0.2);
94
+ const sg = Math.round(ng * 0.2);
95
+ const sb = Math.round(nb * 0.2);
96
+ this.borderColor = (str: string) =>
97
+ `\x1b[48;2;${sr};${sg};${sb}m\x1b[38;2;${nr};${ng};${nb}m${str}\x1b[0m`;
98
+ }
99
+ try {
100
+ return origRender.call(this, width);
101
+ } finally {
102
+ this.borderColor = saved;
103
+ }
104
+ }
105
+ return origRender.call(this, width);
106
+ };
107
+
108
+ // ═══════════════════════════════════════════════════════════════════════════
109
+ // Extension
110
+ // ═══════════════════════════════════════════════════════════════════════════
111
+
112
+ export default function (pi: ExtensionAPI) {
113
+ let thinkingLevel = "off";
114
+ let requestRender: (() => void) | undefined;
115
+ const refresh = () => requestRender?.();
116
+
117
+ pi.on("session_start", async (_event, ctx) => {
118
+ thinkingLevel = pi.getThinkingLevel();
119
+
120
+ // 隐藏内置的 "Working..." 指示器,用输入框呼吸代替
121
+ ctx.ui.setWorkingVisible(false);
122
+
123
+ ctx.ui.setFooter((tui, theme, footerData) => {
124
+ requestRender = () => tui.requestRender();
125
+ const unsubBranch = footerData.onBranchChange(() => tui.requestRender());
126
+ const clock = setInterval(() => tui.requestRender(), 30_000);
127
+
128
+ return {
129
+ dispose() { unsubBranch(); clearInterval(clock); },
130
+ invalidate() {},
131
+ render(width: number): string[] {
132
+ if (width <= 0) return [];
133
+
134
+ // 分隔符:dim 色(低调,不抢眼)
135
+ const S = ` ${theme.fg("dim", icons.sep)} `;
136
+ const parts: string[] = [];
137
+
138
+ // 1. 模型 —— #ff3cac 热粉色
139
+ parts.push(ansi(C.model, `${icons.model} ${shorten(ctx.model?.id ?? "–")}`));
140
+
141
+ // 2. 思考等级 —— 按等级变色,high/xhigh 彩虹
142
+ parts.push(renderThinkingN(thinkingLevel));
143
+
144
+ // 3. 目录 basename —— #00d4ff 青蓝色
145
+ const dir = ctx.cwd.replace(/\\/g, "/").split("/").filter(Boolean).pop() || ctx.cwd;
146
+ parts.push(ansi(C.path, `${icons.folder} ${dir}`));
147
+
148
+ // 4. 上下文用量 —— #6c5ce7 紫 / #fdcb6e 黄 / #ff3366 红
149
+ parts.push(renderContextN(ctx));
150
+
151
+ // 5. Token 用量 —— #00ff87 荧光绿
152
+ const { input, cost } = calcTotals(ctx);
153
+ parts.push(ansi(C.tokens, `${icons.cache} ${icons.input} ${fmt(input)}`));
154
+
155
+ // 6. 费用 —— #fdcb6e 明黄色
156
+ parts.push(ansi(C.cost, `${icons.cost} ${cost.toFixed(2)}`));
157
+
158
+ return [truncateToWidth(parts.join(S), width, "")];
159
+ },
160
+ };
161
+ });
162
+ });
163
+
164
+ // ── 事件订阅 ──
165
+
166
+ pi.on("thinking_level_select", (event) => {
167
+ thinkingLevel = event.level;
168
+ refresh();
169
+ });
170
+ pi.on("model_select", () => refresh());
171
+
172
+ // 工作状态切换 → 控制呼吸发光
173
+ pi.on("agent_start", () => {
174
+ working = true;
175
+ // 50ms 间隔主动刷新,保证呼吸动画 20fps 流畅
176
+ if (requestRender) {
177
+ animInterval = setInterval(requestRender, 50);
178
+ }
179
+ refresh();
180
+ });
181
+ pi.on("agent_end", () => {
182
+ working = false;
183
+ if (animInterval) {
184
+ clearInterval(animInterval);
185
+ animInterval = undefined;
186
+ }
187
+ refresh();
188
+ });
189
+
190
+ pi.on("turn_start", () => refresh());
191
+ pi.on("turn_end", () => refresh());
192
+
193
+ pi.on("session_shutdown", (_event, ctx) => {
194
+ ctx.ui.setFooter(undefined);
195
+ requestRender = undefined;
196
+ if (animInterval) {
197
+ clearInterval(animInterval);
198
+ animInterval = undefined;
199
+ }
200
+ });
201
+ }
202
+
203
+ // ═══════════════════════════════════════════════════════════════════════════
204
+ // Helper 函数
205
+ // ═══════════════════════════════════════════════════════════════════════════
206
+
207
+ /** 16 进制色值 → ANSI 24-bit true color 前景色转义序列 */
208
+ function ansi(hex: string, text: string): string {
209
+ const h = hex.replace("#", "");
210
+ const r = parseInt(h.slice(0, 2), 16);
211
+ const g = parseInt(h.slice(2, 4), 16);
212
+ const b = parseInt(h.slice(4, 6), 16);
213
+ return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
214
+ }
215
+
216
+ /** 渲染思考等级 */
217
+ function renderThinkingN(level: string): string {
218
+ const label = level === "minimal" ? "min"
219
+ : level === "medium" ? "med"
220
+ : level;
221
+ const text = `think:${label}`;
222
+
223
+ switch (level) {
224
+ case "high":
225
+ case "xhigh":
226
+ return rainbow(text);
227
+ case "minimal":
228
+ return ansi(C.thinkingMinimal, text);
229
+ case "low":
230
+ return ansi(C.thinkingLow, text);
231
+ case "medium":
232
+ return ansi(C.thinkingMedium, text);
233
+ default:
234
+ return ansi(C.thinking, text);
235
+ }
236
+ }
237
+
238
+ /** 渲染上下文用量,带百分比阈值颜色 */
239
+ function renderContextN(ctx: any): string {
240
+ const usage = ctx.getContextUsage();
241
+ const pct = usage?.percent;
242
+ const color = pct == null ? C.context
243
+ : pct >= 90 ? C.contextError
244
+ : pct >= 70 ? C.contextWarn
245
+ : C.context;
246
+ const maxStr = ctx.model?.contextWindow
247
+ ? `/${(ctx.model.contextWindow / 1_000_000).toFixed(1)}M`
248
+ : "";
249
+ return ansi(color, `${icons.context} ${pct?.toFixed(1) ?? "?"}%${maxStr}`);
250
+ }
251
+
252
+ /** 遍历会话分支,累计 input token 数和总费用 */
253
+ function calcTotals(ctx: any): { input: number; cost: number } {
254
+ let input = 0, cost = 0;
255
+ for (const e of ctx.sessionManager.getBranch()) {
256
+ if (e.type === "message" && e.message.role === "assistant") {
257
+ const m = e.message as AssistantMessage;
258
+ input += m.usage.input ?? 0;
259
+ cost += m.usage.cost?.total ?? 0;
260
+ }
261
+ }
262
+ return { input, cost };
263
+ }
264
+
265
+ /** 格式化数字:1.2k / 45M */
266
+ function fmt(n: number): string {
267
+ if (n < 1000) return `${n}`;
268
+ if (n < 1_000_000) return `${(n / 1000).toFixed(n < 10_000 ? 1 : 0)}k`;
269
+ return `${(n / 1_000_000).toFixed(1)}m`;
270
+ }
271
+
272
+ /** 缩短模型名 */
273
+ function shorten(m: string): string {
274
+ return m
275
+ .replace(/^claude-/i, "")
276
+ .replace(/^gpt-/i, "gpt ")
277
+ .replace(/-20\d{6}$/, "")
278
+ .replace(/-latest$/i, "");
279
+ }
280
+
281
+ /** 彩虹渐变 ANSI 渲染 */
282
+ function rainbow(text: string): string {
283
+ let out = "", ci = 0;
284
+ for (const ch of text) {
285
+ if (ch === " " || ch === ":") {
286
+ out += ch;
287
+ } else {
288
+ const hex = RAINBOW[ci++ % RAINBOW.length].replace("#", "");
289
+ const r = parseInt(hex.slice(0, 2), 16);
290
+ const g = parseInt(hex.slice(2, 4), 16);
291
+ const b = parseInt(hex.slice(4, 6), 16);
292
+ out += `\x1b[38;2;${r};${g};${b}m${ch}\x1b[0m`;
293
+ }
294
+ }
295
+ return out;
296
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lokiyou/pi-nano-footer",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "超轻量 powerline 风格 footer for Pi Coding Agent — 输入框脉冲发光,霓虹配色,实时显示模型、目录、上下文、token 和费用",
5
5
  "type": "module",
6
6
  "keywords": [