@lokiyou/pi-nano-footer 0.1.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 (3) hide show
  1. package/README.md +45 -0
  2. package/index.ts +211 -0
  3. package/package.json +37 -0
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # @lokiyou/pi-nano-footer
2
+
3
+ 超轻量 powerline 风格 footer for **Pi Coding Agent**,霓虹配色方案。
4
+
5
+ 精确复刻 pi-powerline-footer default 预设的样式 + 自定义霓虹色配色。
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ pi install npm:@lokiyou/pi-nano-footer
11
+ ```
12
+
13
+ 或手动放入 `~/.pi/agent/extensions/` 后 `/reload`。
14
+
15
+ ## 效果
16
+
17
+ ```
18
+  deepseek-v4-flash  think:high   project   45.2%/128K    1.2k   0.03
19
+ ```
20
+
21
+ 从左到右依次展示:
22
+ - 🤖 **模型名**(热粉色)
23
+ - 💭 **思考等级**(按等级变色,high/xhigh 彩虹)
24
+ - 📁 **目录名**(青蓝色)
25
+ - 📊 **上下文用量**(紫 → 黄 >70% → 红 >90%)
26
+ - 💾 **Token 用量**(荧光绿)
27
+ - 💰 **费用**(明黄色)
28
+
29
+ ## 配色
30
+
31
+ | 令牌 | 色值 | 用途 |
32
+ |------|------|------|
33
+ | 模型 | `#ff3cac` | 热粉 |
34
+ | 路径 | `#00d4ff` | 青蓝 |
35
+ | thinking | `#ff6b6b` | 珊瑚红 |
36
+ | thinking high/xhigh | rainbow | 彩虹渐变 |
37
+ | 上下文正常 | `#6c5ce7` | 紫色 |
38
+ | 上下文 >70% | `#fdcb6e` | 黄色警告 |
39
+ | 上下文 >90% | `#ff3366` | 红色错误 |
40
+ | 费用 | `#fdcb6e` | 明黄 |
41
+ | tokens | `#00ff87` | 荧光绿 |
42
+
43
+ ## 许可
44
+
45
+ MIT
package/index.ts ADDED
@@ -0,0 +1,211 @@
1
+ /**
2
+ * pi-nano-footer — 超轻量 powerline 风格 footer
3
+ *
4
+ * 精确复刻 pi-powerline-footer default 预设的样式 +
5
+ * 用户自定义霓虹色配色方案。
6
+ *
7
+ * 安装:放到 ~/.pi/agent/extensions/ 后重启 pi 即可自动加载。
8
+ * 临时测试:pi -e ~/.pi/agent/extensions/pi-nano-footer.ts
9
+ */
10
+
11
+ import type { AssistantMessage } from "@earendil-works/pi-ai";
12
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
+ import { truncateToWidth } from "@earendil-works/pi-tui";
14
+
15
+ // ── Nerd Font 图标(与 pi-powerline-footer 完全一致) ──
16
+ const icons = {
17
+ model: "\uec19", // nf-md-chip
18
+ folder: "\uf115", // nf-fa-folder_open
19
+ context: "\ue70f", // nf-dev-database
20
+ cache: "\uf1c0", // nf-fa-database
21
+ input: "\uf090", // nf-fa-sign_in
22
+ cost: "\uf155", // nf-fa-dollar
23
+ sep: "\ue0b1", // powerline-thin
24
+ };
25
+
26
+ // ── 用户自定义霓虹配色 ──
27
+ const C = {
28
+ model: "#ff3cac",
29
+ shellMode: "#00ff87",
30
+ path: "#00d4ff",
31
+ gitDirty: "#ff9f1c",
32
+ gitClean: "#00ff87",
33
+ thinking: "#ff6b6b",
34
+ thinkingMinimal: "#a855f7",
35
+ thinkingLow: "#00d4ff",
36
+ thinkingMedium: "#ff9f1c",
37
+ context: "#6c5ce7",
38
+ contextWarn: "#fdcb6e",
39
+ contextError: "#ff3366",
40
+ cost: "#fdcb6e",
41
+ tokens: "#00ff87",
42
+ separator: "#a855f7",
43
+ border: "#dfe6e9",
44
+ };
45
+
46
+ // ── Rainbow 色表(high / xhigh 思考等级用) ──
47
+ const RAINBOW = [
48
+ "#b281d6", "#d787af", "#febc38", "#e4c00f",
49
+ "#89d281", "#00afaf", "#178fb9", "#b281d6",
50
+ ];
51
+
52
+ export default function (pi: ExtensionAPI) {
53
+ let thinkingLevel = "off";
54
+ let requestRender: (() => void) | undefined;
55
+ const refresh = () => requestRender?.();
56
+
57
+ pi.on("session_start", async (_event, ctx) => {
58
+ thinkingLevel = pi.getThinkingLevel();
59
+
60
+ ctx.ui.setFooter((tui, theme, footerData) => {
61
+ requestRender = () => tui.requestRender();
62
+ const unsubBranch = footerData.onBranchChange(() => tui.requestRender());
63
+ const clock = setInterval(() => tui.requestRender(), 30_000);
64
+
65
+ return {
66
+ dispose() { unsubBranch(); clearInterval(clock); },
67
+ invalidate() {},
68
+ render(width: number): string[] {
69
+ if (width <= 0) return [];
70
+
71
+ // 分隔符:dim 色(低调,不抢眼)
72
+ const S = ` ${theme.fg("dim", icons.sep)} `;
73
+ const parts: string[] = [];
74
+
75
+ // 1. 模型 —— #ff3cac 热粉色
76
+ parts.push(ansi(C.model, `${icons.model} ${shorten(ctx.model?.id ?? "–")}`));
77
+
78
+ // 2. 思考等级 —— 按等级变色,high/xhigh 彩虹
79
+ parts.push(renderThinkingN(thinkingLevel));
80
+
81
+ // 3. 目录 basename —— #00d4ff 青蓝色
82
+ const dir = ctx.cwd.replace(/\\/g, "/").split("/").filter(Boolean).pop() || ctx.cwd;
83
+ parts.push(ansi(C.path, `${icons.folder} ${dir}`));
84
+
85
+ // 4. 上下文用量 —— #6c5ce7 紫 / #fdcb6e 黄 / #ff3366 红
86
+ parts.push(renderContextN(ctx));
87
+
88
+ // 5. Cache read —— #00ff87 荧光绿
89
+ const { input, cost } = calcTotals(ctx);
90
+ parts.push(ansi(C.tokens, `${icons.cache} ${icons.input} ${fmt(input)}`));
91
+
92
+ // 6. 费用 —— #fdcb6e 明黄色
93
+ parts.push(ansi(C.cost, `${icons.cost} ${cost.toFixed(2)}`));
94
+
95
+ return [truncateToWidth(parts.join(S), width, "")];
96
+ },
97
+ };
98
+ });
99
+ });
100
+
101
+ // ── 事件订阅:保持 footer 实时刷新 ──
102
+
103
+ pi.on("thinking_level_select", (event) => {
104
+ thinkingLevel = event.level;
105
+ refresh();
106
+ });
107
+ pi.on("model_select", () => refresh());
108
+ pi.on("turn_start", () => refresh());
109
+ pi.on("turn_end", () => refresh());
110
+ pi.on("agent_end", () => refresh());
111
+
112
+ pi.on("session_shutdown", (_event, ctx) => {
113
+ ctx.ui.setFooter(undefined);
114
+ requestRender = undefined;
115
+ });
116
+ }
117
+
118
+ // ═══════════════════════════════════════════════════════════════════════════
119
+ // Helper 函数
120
+ // ═══════════════════════════════════════════════════════════════════════════
121
+
122
+ /** 16 进制色值 → ANSI 24-bit true color 前景色转义序列 */
123
+ function ansi(hex: string, text: string): string {
124
+ const h = hex.replace("#", "");
125
+ const r = parseInt(h.slice(0, 2), 16);
126
+ const g = parseInt(h.slice(2, 4), 16);
127
+ const b = parseInt(h.slice(4, 6), 16);
128
+ return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
129
+ }
130
+
131
+ /** 渲染思考等级 */
132
+ function renderThinkingN(level: string): string {
133
+ const label = level === "minimal" ? "min"
134
+ : level === "medium" ? "med"
135
+ : level;
136
+ const text = `think:${label}`;
137
+
138
+ switch (level) {
139
+ case "high":
140
+ case "xhigh":
141
+ return rainbow(text);
142
+ case "minimal":
143
+ return ansi(C.thinkingMinimal, text);
144
+ case "low":
145
+ return ansi(C.thinkingLow, text);
146
+ case "medium":
147
+ return ansi(C.thinkingMedium, text);
148
+ default:
149
+ return ansi(C.thinking, text);
150
+ }
151
+ }
152
+
153
+ /** 渲染上下文用量,带百分比阈值颜色 */
154
+ function renderContextN(ctx: any): string {
155
+ const usage = ctx.getContextUsage();
156
+ const pct = usage?.percent;
157
+ const color = pct == null ? C.context
158
+ : pct >= 90 ? C.contextError
159
+ : pct >= 70 ? C.contextWarn
160
+ : C.context;
161
+ const maxStr = ctx.model?.contextWindow
162
+ ? `/${(ctx.model.contextWindow / 1_000_000).toFixed(1)}M`
163
+ : "";
164
+ return ansi(color, `${icons.context} ${pct?.toFixed(1) ?? "?"}%${maxStr}`);
165
+ }
166
+
167
+ /** 遍历会话分支,累计 input token 数和总费用 */
168
+ function calcTotals(ctx: any): { input: number; cost: number } {
169
+ let input = 0, cost = 0;
170
+ for (const e of ctx.sessionManager.getBranch()) {
171
+ if (e.type === "message" && e.message.role === "assistant") {
172
+ const m = e.message as AssistantMessage;
173
+ input += m.usage.input ?? 0;
174
+ cost += m.usage.cost?.total ?? 0;
175
+ }
176
+ }
177
+ return { input, cost };
178
+ }
179
+
180
+ /** 格式化数字:1.2k / 45M */
181
+ function fmt(n: number): string {
182
+ if (n < 1000) return `${n}`;
183
+ if (n < 1_000_000) return `${(n / 1000).toFixed(n < 10_000 ? 1 : 0)}k`;
184
+ return `${(n / 1_000_000).toFixed(1)}m`;
185
+ }
186
+
187
+ /** 缩短模型名 */
188
+ function shorten(m: string): string {
189
+ return m
190
+ .replace(/^claude-/i, "")
191
+ .replace(/^gpt-/i, "gpt ")
192
+ .replace(/-20\d{6}$/, "")
193
+ .replace(/-latest$/i, "");
194
+ }
195
+
196
+ /** 彩虹渐变 ANSI 渲染 */
197
+ function rainbow(text: string): string {
198
+ let out = "", ci = 0;
199
+ for (const ch of text) {
200
+ if (ch === " " || ch === ":") {
201
+ out += ch;
202
+ } else {
203
+ const hex = RAINBOW[ci++ % RAINBOW.length].replace("#", "");
204
+ const r = parseInt(hex.slice(0, 2), 16);
205
+ const g = parseInt(hex.slice(2, 4), 16);
206
+ const b = parseInt(hex.slice(4, 6), 16);
207
+ out += `\x1b[38;2;${r};${g};${b}m${ch}\x1b[0m`;
208
+ }
209
+ }
210
+ return out;
211
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@lokiyou/pi-nano-footer",
3
+ "version": "0.1.0",
4
+ "description": "超轻量 powerline 风格 footer for Pi Coding Agent — 霓虹配色,实时显示模型、思考等级、目录、上下文、token 和费用",
5
+ "type": "module",
6
+ "keywords": [
7
+ "pi-package",
8
+ "pi-extension",
9
+ "footer",
10
+ "statusline",
11
+ "powerline",
12
+ "neon"
13
+ ],
14
+ "author": "lokiyou",
15
+ "license": "MIT",
16
+ "files": [
17
+ "index.ts",
18
+ "README.md"
19
+ ],
20
+ "pi": {
21
+ "extensions": [
22
+ "./index.ts"
23
+ ]
24
+ },
25
+ "peerDependencies": {
26
+ "@earendil-works/pi-coding-agent": "*",
27
+ "@earendil-works/pi-tui": "*",
28
+ "@earendil-works/pi-ai": "*"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": ""
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }