@lokiyou/pi-nano-footer 0.15.2 → 0.15.4

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 +49 -15
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -14,6 +14,7 @@ const icons = {
14
14
  model: "\uec19", // nf-md-chip
15
15
  mcp: "\u{f048d}", // nf-md-server-network
16
16
  folder: "\uf115", // nf-fa-folder_open
17
+ thinking: "\ueab4", // nf-mdi-lightbulb
17
18
  context: "\ue70f", // nf-dev-database
18
19
  cache: "\uf1c0", // nf-fa-database
19
20
  input: "\uf090", // nf-fa-sign_in
@@ -66,10 +67,22 @@ export default function (pi: ExtensionAPI) {
66
67
  ctx.ui.setFooter((tui, theme, footerData) => {
67
68
  requestRender = () => tui.requestRender();
68
69
  const unsubBranch = footerData.onBranchChange(() => tui.requestRender());
70
+ let lastStatusSnapshot = snapshotStatuses(footerData);
71
+ const statusPoll = setInterval(() => {
72
+ const nextStatusSnapshot = snapshotStatuses(footerData);
73
+ if (nextStatusSnapshot !== lastStatusSnapshot) {
74
+ lastStatusSnapshot = nextStatusSnapshot;
75
+ tui.requestRender();
76
+ }
77
+ }, 500);
69
78
  const clock = setInterval(() => tui.requestRender(), 30_000);
70
79
 
71
80
  return {
72
- dispose() { unsubBranch(); clearInterval(clock); },
81
+ dispose() {
82
+ unsubBranch();
83
+ clearInterval(statusPoll);
84
+ clearInterval(clock);
85
+ },
73
86
  invalidate() {},
74
87
  render(width: number): string[] {
75
88
  if (width <= 0) return [];
@@ -124,10 +137,24 @@ export default function (pi: ExtensionAPI) {
124
137
  });
125
138
  }
126
139
 
140
+ function snapshotStatuses(footerData: any): string {
141
+ const statuses = footerData?.getExtensionStatuses?.();
142
+ if (!statuses || typeof statuses.entries !== "function") return "";
143
+
144
+ return Array.from(statuses.entries())
145
+ .sort(([a], [b]) => String(a).localeCompare(String(b)))
146
+ .map(([key, value]) => `${String(key)}:${stripAnsi(String(value))}`)
147
+ .join("|");
148
+ }
149
+
127
150
  // ═══════════════════════════════════════════════════════════════════════════
128
151
  // Helper 函数
129
152
  // ═══════════════════════════════════════════════════════════════════════════
130
153
 
154
+ function stripAnsi(text: string): string {
155
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
156
+ }
157
+
131
158
  /** 16 进制色值 → ANSI 24-bit true color 前景色转义序列 */
132
159
  function ansi(hex: string, text: string): string {
133
160
  const h = hex.replace("#", "");
@@ -137,25 +164,24 @@ function ansi(hex: string, text: string): string {
137
164
  return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
138
165
  }
139
166
 
167
+ function paint(hex: string, text: string, bold = false): string {
168
+ return bold ? `\x1b[1m${ansi(hex, text)}` : ansi(hex, text);
169
+ }
170
+
140
171
  /** 渲染思考等级 */
141
172
  function renderThinkingN(level: string): string {
142
- const label = level === "minimal" ? "min"
143
- : level === "medium" ? "med"
144
- : level;
145
- const text = `think:${label}`;
146
-
147
173
  switch (level) {
148
174
  case "high":
149
175
  case "xhigh":
150
- return rainbow(text);
151
- case "minimal":
152
- return ansi(C.thinkingMinimal, text);
153
- case "low":
154
- return ansi(C.thinkingLow, text);
176
+ return paint("#fff6b0", icons.thinking, true);
155
177
  case "medium":
156
- return ansi(C.thinkingMedium, text);
178
+ return paint(C.thinkingMedium, icons.thinking, true);
179
+ case "low":
180
+ return paint(C.thinkingLow, icons.thinking);
181
+ case "minimal":
182
+ return paint(C.thinkingMinimal, icons.thinking);
157
183
  default:
158
- return ansi(C.thinking, text);
184
+ return paint(C.thinking, icons.thinking);
159
185
  }
160
186
  }
161
187
 
@@ -168,7 +194,7 @@ function renderContextN(ctx: any): string {
168
194
  : pct >= 70 ? C.contextWarn
169
195
  : C.context;
170
196
  const maxStr = ctx.model?.contextWindow
171
- ? `/${(ctx.model.contextWindow / 1_000_000).toFixed(1)}M`
197
+ ? `/${formatContextWindow(ctx.model.contextWindow)}`
172
198
  : "";
173
199
  return ansi(color, `${icons.context} ${pct?.toFixed(1) ?? "?"}%${maxStr}`);
174
200
  }
@@ -183,7 +209,7 @@ function renderMcpN(footerData: any): string | null {
183
209
  : Array.from(statuses.values()).find((value: unknown) => typeof value === "string" && value.includes("MCP:"));
184
210
  if (typeof raw !== "string") return null;
185
211
 
186
- const status = raw.replace(/\x1b\[[0-9;]*m/g, "").replace(/\s+/g, " ").trim();
212
+ const status = stripAnsi(raw).replace(/\s+/g, " ").trim();
187
213
 
188
214
  const ratio = /MCP:\s*(\d+)\s*\/\s*(\d+)\s+servers?/i.exec(status);
189
215
  if (ratio) {
@@ -230,6 +256,14 @@ function fmt(n: number): string {
230
256
  return `${(n / 1_000_000).toFixed(1)}m`;
231
257
  }
232
258
 
259
+ function formatContextWindow(n: number): string {
260
+ if (n < 1000) return `${n}`;
261
+ if (n < 1_000_000) return `${Math.floor(n / 1000)}K`;
262
+ const scaled = n / 1_000_000;
263
+ const text = Number.isInteger(scaled) ? `${scaled.toFixed(0)}` : scaled.toFixed(scaled >= 10 ? 0 : 1).replace(/\.0$/, "");
264
+ return `${text}M`;
265
+ }
266
+
233
267
  /** 缩短模型名 */
234
268
  function shorten(m: string): string {
235
269
  return m
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lokiyou/pi-nano-footer",
3
- "version": "0.15.2",
3
+ "version": "0.15.4",
4
4
  "description": "Lightweight powerline-style footer for Pi Coding Agent that shows model, thinking level, directory, MCP status, context usage, tokens, and cost while keeping the built-in Working indicator.",
5
5
  "type": "module",
6
6
  "keywords": [