@lokiyou/pi-nano-footer 0.10.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.
- package/index.ts +296 -292
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -1,292 +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
|
-
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.
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
parts.push(
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
parts.push(
|
|
150
|
-
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
pi.on("
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
case "
|
|
226
|
-
return
|
|
227
|
-
case "
|
|
228
|
-
return ansi(C.
|
|
229
|
-
|
|
230
|
-
return ansi(C.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
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
|
+
}
|