@yalieny/pi-better-cost-display-footer 2.2.0 → 2.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.
- package/README.md +33 -0
- package/extensions/pi-better-cost-display-footer.ts +440 -171
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -109,7 +109,40 @@ node --experimental-strip-types test/drift-probe.ts # 端到端:守卫重
|
|
|
109
109
|
- 模型缺少当前档位价格时保持原价并打日志。
|
|
110
110
|
- cost 仅影响 pi 本地用量统计,不影响 API 实际账单。
|
|
111
111
|
|
|
112
|
+
## footer 与 pi 升级
|
|
113
|
+
|
|
114
|
+
本扩展不再复刻 pi 的 footer。它在 `session_start` 加载已安装 pi 自己的
|
|
115
|
+
`dist/modes/interactive/components/footer.js`(路径由 `pi` 的 bin 反推),把内置的
|
|
116
|
+
`FooterComponent` 包一层,只在**渲染结果上打补丁**:
|
|
117
|
+
|
|
118
|
+
1. 金额符号(`currencySymbol`,内置硬编码 `$`)。
|
|
119
|
+
2. CH 小数位(`cacheHitRatePrecision`,内置硬编码 1 位)。
|
|
120
|
+
3. context 占用条(`█░░░░░░░░░ 10.5%/1.0M`;外观抄 `pi-cc-extensions` 的 `barGauge`,≥80% 转 warning、≥95% 转 error)。
|
|
121
|
+
4. 档位标签(内置没有)。
|
|
122
|
+
5. tok/s 速率段(内置没有)。
|
|
123
|
+
6. pwd 行:cwd 只显示最底层目录(抄 cc),分支换成 `⎇ 分支` 底色 chip(图标抄 look pack),后跟 git 变更行数 `+12 −3`(数据与刷新抄 cc:`git diff --numstat HEAD`,启动查一次 + 10s 轮询 + 分支变化重查,跳过二进制行)。
|
|
124
|
+
7. 右侧:模型(含 `(provider)` 前缀)上底色 chip(抄 look pack),thinking 级别上色(抄 cc;分隔符沿用内置的 `•`)。
|
|
125
|
+
|
|
126
|
+
pwd 行、统计段布局、对齐、截断、状态行、以及 pi 以后新增的字段,全部用内置渲染:
|
|
127
|
+
pi 改 footer 会自动跟随,不会用旧版布局覆盖。补丁只改行的文本,并按补白收缩保证行宽不变。
|
|
128
|
+
空间不够时按 速率 → 标签 → 占用条 的顺序舍弃。
|
|
129
|
+
|
|
130
|
+
降级(都不会弄坏 footer):
|
|
131
|
+
|
|
132
|
+
- 拿不到内置组件(单文件二进制、源码运行、构造失败)→ 不替换,直接用 pi 自带 footer,并提醒一次。
|
|
133
|
+
- 内置 stats 行结构不认识(补丁打不上)→ 内置原样渲染,提醒一次;`/cost-tier` 的 `footer:` 一行给出状态。
|
|
134
|
+
|
|
135
|
+
## footer 控制权(与其他扩展共存)
|
|
136
|
+
|
|
137
|
+
pi 没有“叠加 footer”的扩展点。两家扩展都调 `setFooter` 时,后调用的直接覆盖前一个
|
|
138
|
+
(顺序 = `settings.packages` 顺序)。已知的竞争者:`pi-cc-extensions`(`enableCustomFooter: true`,
|
|
139
|
+
两行 chips 状态栏)与 `awesome-pi-themes` 的 look pack。
|
|
140
|
+
|
|
141
|
+
本插件是通用插件,不做针对某个扩展的特例,总是接管 footer。要同时用对方的 footer,
|
|
142
|
+
就关掉一个(cc:`/ccstyle`,或 `"enableCustomFooter": false`)。
|
|
143
|
+
|
|
112
144
|
## 变更记录
|
|
113
145
|
|
|
146
|
+
- **2.3.0** footer 改为“增量补丁”而非整段复刻:加载并包装 pi 自己的 `FooterComponent`,只在渲染结果上换金额符号、改 CH 小数位、加 context 占用条、加档位标签与 tok/s;pwd 行改为“`⎇ 分支` 底色 chip + git 变更行数”,右侧模型上 chip、thinking 上色(分隔符沿用内置)。占用条抄 `pi-cc-extensions` 的 `barGauge`,分支图标与分隔符抄 look pack,git 变更行数(含 10s 轮询与分支变化重查)抄 cc。pi 升级 footer(新字段、新布局)自动跟随,不再覆盖。拿不到内置组件或布局变化时降级为 pi 自带 footer 并提醒一次。
|
|
114
147
|
- **2.2.0** footer 新增 tok/s 速率(思考/正文分相位):流式中显示当前相位实时值 `⚡45.2t/s`(无前缀,2s 滚动窗口),assistant `message_end` 用真实 `usage` 冻结为固定三档 `⚡(175.1/r31.8/o613.4)t/s`(总/思考/正文,无值打 `-` 占位);分母从各相位首 delta 起算,不含首 token 延迟与工具执行时长(定稿用 message_end 而非 turn_end)。实时值为估算,思考/正文各自的折算比由真实 token EMA 自校准(初始 4,限幅 0.5~8);异常速率(>1000 t/s)丢弃;无效 `usage`(失败/中止)不覆盖定稿值。/cost-tier 新增两档折算比。
|
|
115
148
|
- **2.1.2** 修复"档位标签正确但计费金额按错档结算":注册守卫改为按实际计费 cost 判定漂移(不再只看档位字符串),注册失败不再写入守卫(下一轮自动重试);漂移 3 次未收敛转为 footer 可见告警(`≈` + 标签 `?`);新增 `/cost-tier` 诊断命令与注册失败一次性提示。方案见 `docs/fix-plan-stale-cost-tier.md`。
|
|
@@ -35,12 +35,15 @@
|
|
|
35
35
|
// (不再限定 deepseek 官方,其余 provider 同样适用)。
|
|
36
36
|
// - 自定义 footer 与 CH 精度:所有 provider 适用;未配置模型仅修改 CH,其余保持内置默认。
|
|
37
37
|
//
|
|
38
|
-
//
|
|
39
|
-
// 复刻内置 footer 渲染(footer.js),并加两处增强:
|
|
38
|
+
// 计费显示:复用 pi 自己的 footer 组件(ctx.ui.setFooter 包一层),只在渲染结果上打补丁:
|
|
40
39
|
// 1. 激活模型有动态计价配置时,价格后紧跟档位标签(labels 按档位 id 查,未配置则不显示标签):
|
|
41
40
|
// 如 ¥0.047(梁文峰)。color 支持主题色名或 #rrggbb。
|
|
42
41
|
// 2. CH 缓存命中率小数位由 cacheHitRatePrecision 控制(内置硬编码 1 位),所有 provider 适用。
|
|
43
42
|
// 3. 计费金额符号由各 provider 的 currencySymbol 控制(内置硬编码 $)。
|
|
43
|
+
// 4. tok/s 速率段(本插件新增)。
|
|
44
|
+
// 布局、对齐、截断、pi 新增字段全部沿用内置渲染:pi 升级 footer 自动跟随,不会用旧版布局覆盖。
|
|
45
|
+
// 与其他扩展抢 footer:pi 没有“叠加”扩展点,两家都调 setFooter 时后调用的直接覆盖前一个;
|
|
46
|
+
// 本插件总是接管 footer(通用插件,不针对某个扩展写特例)。
|
|
44
47
|
//
|
|
45
48
|
// 注意:
|
|
46
49
|
// - 旧版顶层计价字段与 provider 级 peakWindows 首次读取时自动迁移并写回配置文件。
|
|
@@ -48,15 +51,17 @@
|
|
|
48
51
|
// - provider 必须在 models.json 中有定义:扩展读取其完整模型定义,仅替换 cost 后整体重注册
|
|
49
52
|
// (registerProvider 的 models 是整体替换语义)。未列出的模型保持原价。
|
|
50
53
|
// - cost 仅影响 pi 本地用量统计,不影响 API 实际账单。
|
|
51
|
-
// - ponytail:
|
|
52
|
-
//
|
|
53
|
-
// footerData
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
57
|
-
import {
|
|
54
|
+
// - ponytail: footer 用“加载 pi 自己的 FooterComponent + 打补丁”,不再整段复刻布局。
|
|
55
|
+
// 依赖 3 个内部约定:<pi>/dist/modes/interactive/components/footer.js 路径、
|
|
56
|
+
// FooterComponent(session, footerData) 构造、stats 行 = dim(统计段) + dim(补白 + 右侧)。
|
|
57
|
+
// 任一不成立 → 补丁不生效(一次性提醒),footer 仍由 pi 自己渲染;主题经 globalThis Proxy 共享,颜色一致。
|
|
58
|
+
// 上限 = 内置无“统计段追加”扩展点;升级路径 = 给内置 footer 增加 stats 追加 hook。
|
|
59
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, realpathSync } from "node:fs";
|
|
60
|
+
import { execFile } from "node:child_process";
|
|
61
|
+
import { join, dirname } from "node:path";
|
|
58
62
|
import { homedir } from "node:os";
|
|
59
|
-
import {
|
|
63
|
+
import { pathToFileURL } from "node:url";
|
|
64
|
+
import { visibleWidth, truncateToWidth } from "@earendil-works/pi-tui";
|
|
60
65
|
import type {
|
|
61
66
|
ExtensionAPI,
|
|
62
67
|
ExtensionContext,
|
|
@@ -618,26 +623,172 @@ function patchProviderConfig(
|
|
|
618
623
|
return { ...provider, models };
|
|
619
624
|
}
|
|
620
625
|
|
|
621
|
-
|
|
626
|
+
/* ---------- 复用内置 footer(增量补丁,不整段复刻) ---------- */
|
|
622
627
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
628
|
+
/**
|
|
629
|
+
* pi 的内置 footer 组件(dist/modes/interactive/components/footer.js)。
|
|
630
|
+
* 不再复刻它的布局:直接加载 pi 自己的实现,只在渲染结果上打补丁
|
|
631
|
+
* (金额符号 / CH 精度 / 档位标签 / tok/s)。pi 改 footer 布局时自动跟随。
|
|
632
|
+
* 主题在 globalThis 上以 Proxy 共享,拿到的就是当前主题,颜色一致。
|
|
633
|
+
*/
|
|
634
|
+
interface BaseFooterModule {
|
|
635
|
+
FooterComponent: new (
|
|
636
|
+
session: unknown,
|
|
637
|
+
footerData: unknown,
|
|
638
|
+
) => { render(width: number): string[] };
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** 加载结果:null = 未加载成功(footer 保持 pi 内置) */
|
|
642
|
+
let baseFooter: BaseFooterModule | null = null;
|
|
643
|
+
/** 只尝试加载一次 */
|
|
644
|
+
let baseFooterLoaded = false;
|
|
645
|
+
/** 未加载成功的原因,供 /cost-tier 与一次性提醒 */
|
|
646
|
+
let baseFooterError: string | null = null;
|
|
647
|
+
/** 补丁失效(内置布局不认识)已提醒过 */
|
|
648
|
+
let footerPatchWarned = false;
|
|
649
|
+
/** 内置组件渲染失败已处理过 */
|
|
650
|
+
let footerDisabled = false;
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* 定位已安装 pi 的内置 footer 源码。pi 的 bin 指向 <pkg>/dist/bundle/cli.js,
|
|
654
|
+
* 由此向上最多 6 层找 dist/modes/interactive/components/footer.js。
|
|
655
|
+
* 找不到(单文件二进制、源码运行)→ null。
|
|
656
|
+
*/
|
|
657
|
+
function upstreamFooterFile(): string | null {
|
|
658
|
+
const entry = process.argv[1];
|
|
659
|
+
if (!entry) return null;
|
|
660
|
+
try {
|
|
661
|
+
let dir = dirname(realpathSync(entry));
|
|
662
|
+
for (let i = 0; i < 6; i++) {
|
|
663
|
+
const candidate = join(dir, "dist", "modes", "interactive", "components", "footer.js");
|
|
664
|
+
if (existsSync(candidate)) return candidate;
|
|
665
|
+
dir = dirname(dir);
|
|
666
|
+
}
|
|
667
|
+
} catch {
|
|
668
|
+
// argv[1] 不可解析 → 不定位
|
|
669
|
+
}
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** 加载 pi 内置 footer 组件;失败不影响计价,只退化为 pi 自带 footer */
|
|
674
|
+
async function loadBaseFooter(): Promise<BaseFooterModule | null> {
|
|
675
|
+
if (baseFooterLoaded) return baseFooter;
|
|
676
|
+
baseFooterLoaded = true;
|
|
677
|
+
const file = upstreamFooterFile();
|
|
678
|
+
if (!file) {
|
|
679
|
+
baseFooterError = "未定位到已安装 pi 的 footer.js(单文件二进制或源码运行)";
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
try {
|
|
683
|
+
const mod = (await import(pathToFileURL(file).href)) as unknown as BaseFooterModule;
|
|
684
|
+
if (typeof mod?.FooterComponent === "function") baseFooter = mod;
|
|
685
|
+
else baseFooterError = `${file} 未导出 FooterComponent`;
|
|
686
|
+
} catch (err) {
|
|
687
|
+
baseFooterError = `加载 ${file} 失败:${err instanceof Error ? err.message : String(err)}`;
|
|
688
|
+
}
|
|
689
|
+
return baseFooter;
|
|
629
690
|
}
|
|
630
691
|
|
|
631
|
-
function
|
|
632
|
-
if (
|
|
633
|
-
|
|
634
|
-
const inside =
|
|
635
|
-
rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
636
|
-
return inside ? (rel === "" ? "~" : `~${sep}${rel}`) : cwd;
|
|
692
|
+
function baseFooterStatusText(): string {
|
|
693
|
+
if (baseFooter) return "复用内置 footer;补丁:context 占用条 / 金额符号 / CH 精度 / 档位标签 / tok/s";
|
|
694
|
+
return `未复用内置 footer(${baseFooterError ?? "未尝试加载"})`;
|
|
637
695
|
}
|
|
638
696
|
|
|
639
|
-
|
|
640
|
-
|
|
697
|
+
/**
|
|
698
|
+
* 内置 FooterComponent 需要的 session 视图,数据全部来自扩展 ctx。
|
|
699
|
+
* modelRuntime 未对扩展暴露 → 订阅制保留内置的 kimi-coding 特例。
|
|
700
|
+
*/
|
|
701
|
+
function footerSessionView(ctx: ExtensionContext) {
|
|
702
|
+
const c = () => activeCtx ?? ctx;
|
|
703
|
+
return {
|
|
704
|
+
get state() {
|
|
705
|
+
return { model: c().model, thinkingLevel: c().thinkingLevel };
|
|
706
|
+
},
|
|
707
|
+
sessionManager: {
|
|
708
|
+
getEntries: () => c().sessionManager.getEntries(),
|
|
709
|
+
getCwd: () => c().sessionManager.getCwd(),
|
|
710
|
+
getSessionName: () => c().sessionManager.getSessionName(),
|
|
711
|
+
},
|
|
712
|
+
getContextUsage: () => c().getContextUsage(),
|
|
713
|
+
modelRuntime: { isUsingSubscription: (providerId: string) => providerId === "kimi-coding" },
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// ---------- footer 补丁:占用条 / 金额符号 / CH 精度 / 档位标签 / tok/s / pwd 行 ----------
|
|
718
|
+
|
|
719
|
+
/** 去掉 ANSI 转义,只留可见文本 */
|
|
720
|
+
function stripAnsi(text: string): string {
|
|
721
|
+
return text.replace(/\u001b\[[0-9;]*m/g, "");
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** 分支图标(抄 look pack 的 `⎇`,不依赖 Nerd Font) */
|
|
725
|
+
const BRANCH_ICON = "⎇";
|
|
726
|
+
/** 段分隔符(抄 look pack) */
|
|
727
|
+
const SEG_SEP = " │ ";
|
|
728
|
+
/** git 变更行数刷新间隔(抄 pi-cc-extensions) */
|
|
729
|
+
const GIT_REFRESH_INTERVAL_MS = 10_000;
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* 解析 `git diff --numstat HEAD` 输出:每行前两列是增/删行数,二进制行为 `-\t-` → 跳过。
|
|
733
|
+
* 抄 pi-cc-extensions 的 parseGitStats。
|
|
734
|
+
*/
|
|
735
|
+
export function parseGitStats(stdout: string): { add: number; del: number } {
|
|
736
|
+
let add = 0;
|
|
737
|
+
let del = 0;
|
|
738
|
+
for (const line of stdout.split("\n")) {
|
|
739
|
+
const match = /^(\d+)\s+(\d+)/.exec(line);
|
|
740
|
+
if (!match) continue;
|
|
741
|
+
add += Number(match[1]);
|
|
742
|
+
del += Number(match[2]);
|
|
743
|
+
}
|
|
744
|
+
return { add, del };
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/** 工作区变更行数;undefined = 未查到(非 git 仓库 / 查询失败) */
|
|
748
|
+
let gitStats: { add: number; del: number } | undefined;
|
|
749
|
+
let gitRefreshRunning = false;
|
|
750
|
+
|
|
751
|
+
/** 查一次 `git diff --numstat HEAD`;结果变化时回调(用于重绘) */
|
|
752
|
+
function refreshGitStats(cwd: string, onChanged: () => void): void {
|
|
753
|
+
if (gitRefreshRunning) return;
|
|
754
|
+
gitRefreshRunning = true;
|
|
755
|
+
execFile("git", ["diff", "--numstat", "HEAD"], { cwd, timeout: 2000 }, (err, stdout) => {
|
|
756
|
+
gitRefreshRunning = false;
|
|
757
|
+
if (err) return;
|
|
758
|
+
const next = parseGitStats(stdout);
|
|
759
|
+
if (gitStats?.add === next.add && gitStats.del === next.del) return;
|
|
760
|
+
gitStats = next;
|
|
761
|
+
onChanged();
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/** 分支标签:`⎇ branch` 做成 accent 底色 chip,后跟 git 变更行数(success/error) */
|
|
766
|
+
function branchLabel(branch: string, stats: { add: number; del: number } | null | undefined, theme: Theme): string {
|
|
767
|
+
// 带尾空格的 chip 用于后接 ± 数字;无数字时不带尾空格,避免行末多一个空格
|
|
768
|
+
const chip = (text: string) => theme.bg("selectedBg", theme.fg("accent", text));
|
|
769
|
+
const icon = `${BRANCH_ICON} ${branch}`;
|
|
770
|
+
if (!stats) return chip(icon);
|
|
771
|
+
return `${chip(`${icon} `)}${theme.fg("success", `+${stats.add}`)} ${theme.fg("error", `−${stats.del}`)}`;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/** 内置金额格式 `$0.047` / `$0.047 (sub)`;只改符号,不改数值 */
|
|
775
|
+
const COST_RE = /\$(\d+\.\d+)( \(sub\))?/;
|
|
776
|
+
|
|
777
|
+
/** context 占用条宽度(格) */
|
|
778
|
+
const BAR_WIDTH = 10;
|
|
779
|
+
/** ≥ 80% 转 warning 色,≥ 95% 转 error 色(同 pi-cc-extensions) */
|
|
780
|
+
const BAR_WARNING = 80;
|
|
781
|
+
const BAR_ERROR = 95;
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* context 占用条。外观抄 pi-cc-extensions 的 barGauge(其注释:pi-cc-status 风格):
|
|
785
|
+
* accent 填充 + dim 空槽,高占用转 warning/error 色。
|
|
786
|
+
*/
|
|
787
|
+
export function barGauge(theme: Theme, percent: number): string {
|
|
788
|
+
const clamped = Math.max(0, Math.min(100, percent));
|
|
789
|
+
const filled = Math.round((clamped / 100) * BAR_WIDTH);
|
|
790
|
+
const color = clamped >= BAR_ERROR ? "error" : clamped >= BAR_WARNING ? "warning" : "accent";
|
|
791
|
+
return theme.fg(color, "█".repeat(filled)) + theme.fg("dim", "░".repeat(BAR_WIDTH - filled));
|
|
641
792
|
}
|
|
642
793
|
|
|
643
794
|
/** 标签着色:ThemeColor 名走主题,#rrggbb 走 truecolor 直出(SGR 38;2;r;g;b) */
|
|
@@ -650,19 +801,182 @@ export function colorize(theme: Theme, color: string, text: string): string {
|
|
|
650
801
|
return theme.fg(color as ThemeColor, text);
|
|
651
802
|
}
|
|
652
803
|
|
|
653
|
-
/**
|
|
654
|
-
function
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
804
|
+
/** 漂移时标签加问号:宁可显示“可疑”,不静默显示错数字 */
|
|
805
|
+
function driftLabelText(text: string): string {
|
|
806
|
+
return text.endsWith(")") ? `${text.slice(0, -1)}?)` : `${text}?`;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/** 补丁内容:金额符号 / CH 精度来自配置,档位标签 / tok/s 是本插件新增 */
|
|
810
|
+
interface FooterPatchOptions {
|
|
811
|
+
/** 配置了 currencySymbol 才替换内置的 `$` */
|
|
812
|
+
symbol?: string;
|
|
813
|
+
/** 配置了 cacheHitRatePrecision 才改 CH 小数位(内置硬编码 1 位) */
|
|
814
|
+
precision?: number;
|
|
815
|
+
/** 计费与档位不符 → 金额前加 `≈` */
|
|
816
|
+
approx?: boolean;
|
|
817
|
+
/** tok/s 片段 */
|
|
818
|
+
speed?: string;
|
|
819
|
+
label?: FooterLabel;
|
|
820
|
+
/** context 占用条:true 时把 `10.5%/1.0M` 换成 `████░░░░░░ 10.5%/1.0M` */
|
|
821
|
+
contextBar?: boolean;
|
|
822
|
+
/** pwd 行:分支图标 + 变更行数;null = 无 git 数据 */
|
|
823
|
+
branchStats?: { add: number; del: number } | null;
|
|
824
|
+
/** 右侧:模型 id 上底色 chip,thinking 级别上色(抄 look pack / cc) */
|
|
825
|
+
modelId?: string;
|
|
826
|
+
thinkingLevel?: string;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* 把内置 stats 行切成 `stats + dim(补白 + 右侧模型名)` 三段。
|
|
831
|
+
* 内置结构不认识(行里没有两段 dim)→ null。
|
|
832
|
+
*/
|
|
833
|
+
function splitStatsLine(
|
|
834
|
+
line: string,
|
|
835
|
+
theme: Theme,
|
|
836
|
+
): { stats: string; padding: string; right: string; dimOpen: string } | null {
|
|
837
|
+
const dimOpen = theme.getFgAnsi("dim");
|
|
838
|
+
const cut = line.lastIndexOf(dimOpen);
|
|
839
|
+
if (cut <= 0 || !line.slice(0, cut).includes(dimOpen)) return null;
|
|
840
|
+
const tail = line.slice(cut + dimOpen.length);
|
|
841
|
+
const padding = /^ */.exec(tail)?.[0] ?? "";
|
|
842
|
+
return { stats: line.slice(0, cut), padding, right: tail.slice(padding.length), dimOpen };
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* 增量补丁:只改统计段内的 context 占用条、金额符号、CH 精度,
|
|
847
|
+
* 并按剩余补白追加速率与档位标签;行宽、对齐、截断、pi 新增的字段全部沿用内置结果。
|
|
848
|
+
* 空间不够时按 速率 → 标签 → 占用条 的顺序舍弃;结构不认识 → 返回原行(补丁可丢,布局不坏)。
|
|
849
|
+
*/
|
|
850
|
+
function patchFooterStatsLine(
|
|
851
|
+
line: string,
|
|
852
|
+
theme: Theme,
|
|
853
|
+
opts: FooterPatchOptions,
|
|
854
|
+
): { line: string; patched: boolean } {
|
|
855
|
+
const parts = splitStatsLine(line, theme);
|
|
856
|
+
if (!parts) return { line, patched: false };
|
|
857
|
+
const { stats: original, padding, right, dimOpen } = parts;
|
|
858
|
+
let stats = original;
|
|
859
|
+
// CH 精度先改:它在金额前,改完再定位金额,下标才准
|
|
860
|
+
if (opts.precision !== undefined) {
|
|
861
|
+
stats = stats.replace(
|
|
862
|
+
/CH(\d+(?:\.\d+)?)%/,
|
|
863
|
+
(_m, v: string) => `CH${Number(v).toFixed(opts.precision)}%`,
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
// 金额:`$数字` 换成配置的符号,可加 ≈;前后插入点一并算好
|
|
867
|
+
const cost = COST_RE.exec(stats);
|
|
868
|
+
let costHead = -1;
|
|
869
|
+
let costTail = -1;
|
|
870
|
+
if (cost) {
|
|
871
|
+
const prefix = `${opts.approx ? "≈" : ""}${opts.symbol ?? "$"}`;
|
|
872
|
+
const numStart = cost.index + 1; // `$` 之后
|
|
873
|
+
stats = stats.slice(0, cost.index) + prefix + stats.slice(numStart);
|
|
874
|
+
costHead = cost.index;
|
|
875
|
+
costTail = cost.index + prefix.length + cost[1].length + (cost[2]?.length ?? 0);
|
|
876
|
+
}
|
|
877
|
+
const speed = opts.speed ? `${opts.speed} ` : "";
|
|
878
|
+
const label = opts.label
|
|
879
|
+
? ` ${colorize(theme, opts.label.color, opts.label.text)}${dimOpen}`
|
|
880
|
+
: "";
|
|
881
|
+
// context 占用条:插在百分比前(在金额之后,不影响 costHead/costTail 下标);`?` 时没有百分比,跳过
|
|
882
|
+
const pct = opts.contextBar ? /(\d+(?:\.\d+)?)%\//.exec(stats) : null;
|
|
883
|
+
const bar = pct ? `${barGauge(theme, Number(pct[1]))} ` : "";
|
|
884
|
+
const barAt = pct ? pct.index : -1;
|
|
885
|
+
const build = (withBar: boolean, withSpeed: boolean, withLabel: boolean): string => {
|
|
886
|
+
let out = stats;
|
|
887
|
+
// 占用条先插:它落在金额之后,再按原下标插速率/标签才不错位
|
|
888
|
+
if (withBar && barAt >= 0) out = out.slice(0, barAt) + bar + out.slice(barAt);
|
|
889
|
+
const s = withSpeed ? speed : "";
|
|
890
|
+
const l = withLabel ? label : "";
|
|
891
|
+
if (costTail < 0) {
|
|
892
|
+
// 无计费段(cost 为 0 且非订阅)→ 追到统计段末;tok/s 与统计段同色,标签自带颜色
|
|
893
|
+
const extras = [withSpeed ? theme.fg("dim", speed.trim()) : "", l.trim()].filter(Boolean);
|
|
894
|
+
return extras.length ? `${out} ${extras.join(" ")}` : out;
|
|
895
|
+
}
|
|
896
|
+
return out.slice(0, costHead) + s + out.slice(costHead, costTail) + l + out.slice(costTail);
|
|
897
|
+
};
|
|
898
|
+
const base = visibleWidth(original);
|
|
899
|
+
// 右段:模型(连同内置的 `(provider) ` 前缀)上底色 chip、thinking 级别上色;chip 多占 2 格从补白扣
|
|
900
|
+
const prefix = /^\(([^)]*)\) /.exec(stripAnsi(right));
|
|
901
|
+
const chipTarget = opts.modelId
|
|
902
|
+
? prefix
|
|
903
|
+
? `${prefix[0]}${opts.modelId}`
|
|
904
|
+
: opts.modelId
|
|
905
|
+
: "";
|
|
906
|
+
const chip = chipTarget ? theme.bg("selectedBg", theme.fg("muted", ` ${chipTarget}`)) : "";
|
|
907
|
+
let patchedRight = right;
|
|
908
|
+
if (chipTarget && patchedRight.includes(chipTarget)) patchedRight = patchedRight.replace(chipTarget, chip);
|
|
909
|
+
if (opts.thinkingLevel) {
|
|
910
|
+
const levelText = opts.thinkingLevel === "off" ? "thinking off" : opts.thinkingLevel;
|
|
911
|
+
// 分隔符沿用内置的 ` • `,只把级别上色(宽度不变)
|
|
912
|
+
patchedRight = patchedRight.replace(
|
|
913
|
+
` • ${levelText}`,
|
|
914
|
+
` • ${theme.getThinkingBorderColor(opts.thinkingLevel)(levelText)}`,
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
const rightDelta = patchedRight === right ? 0 : visibleWidth(stripAnsi(patchedRight)) - visibleWidth(stripAnsi(right));
|
|
918
|
+
// 优先顺序:占用条(本插件新增的显示)> 标签 > 速率;补白用于吸收宽度差
|
|
919
|
+
for (const [withBar, withLabel, withSpeed] of [
|
|
920
|
+
[true, true, true],
|
|
921
|
+
[true, true, false],
|
|
922
|
+
[true, false, false],
|
|
923
|
+
[false, false, false],
|
|
924
|
+
] as const) {
|
|
925
|
+
const patchedStats = build(withBar, withLabel, withSpeed);
|
|
926
|
+
const delta = visibleWidth(patchedStats) - base;
|
|
927
|
+
const shrink = Math.max(0, delta) + Math.max(0, rightDelta);
|
|
928
|
+
if (shrink > padding.length) continue;
|
|
929
|
+
// 宽度差为负(改短了)时补回空格,保持行宽不变
|
|
930
|
+
const pad =
|
|
931
|
+
padding.slice(shrink) + " ".repeat(Math.max(0, -delta) + Math.max(0, -rightDelta));
|
|
932
|
+
return { line: patchedStats + dimOpen + pad + patchedRight, patched: true };
|
|
933
|
+
}
|
|
934
|
+
return { line, patched: false };
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* pwd 行补丁(样式抄 cc 与 look pack):分支换成 `⎇ branch` 底色 chip,后跟 git 变更行数。
|
|
939
|
+
* 路径沿用内置的完整显示;无分支时原样返回(调用方靠引用判断)。
|
|
940
|
+
*/
|
|
941
|
+
export function patchFooterPwdLine(line: string, theme: Theme, opts: FooterPatchOptions): string {
|
|
942
|
+
const text = stripAnsi(line); // 内置 pwd 行只有 dim 一种颜色
|
|
943
|
+
const branch = /\(([^()]*)\)(?= •|$)/.exec(text);
|
|
944
|
+
if (!branch) return line;
|
|
945
|
+
const head = text.slice(0, branch.index).trim();
|
|
946
|
+
const tail = text.slice(branch.index + branch[0].length); // " • 会话名" 或空
|
|
658
947
|
return (
|
|
659
|
-
theme.fg("dim",
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
theme.fg("dim", text.slice(idx + marker.length))
|
|
948
|
+
theme.fg("dim", head + SEG_SEP) +
|
|
949
|
+
branchLabel(branch[1], opts.branchStats, theme) +
|
|
950
|
+
(tail ? theme.fg("dim", tail) : "")
|
|
663
951
|
);
|
|
664
952
|
}
|
|
665
953
|
|
|
954
|
+
/**
|
|
955
|
+
* 在整组 footer 行里打补丁:stats 行(补丁主体)+ 第一行(pwd)。
|
|
956
|
+
* patched 只反映 stats 行,因为它是布局变化的探测器(pwd 行变了不算布局变)。
|
|
957
|
+
*/
|
|
958
|
+
export function patchFooterLines(
|
|
959
|
+
lines: string[],
|
|
960
|
+
theme: Theme,
|
|
961
|
+
opts: FooterPatchOptions,
|
|
962
|
+
width = 0,
|
|
963
|
+
): { lines: string[]; patched: boolean } {
|
|
964
|
+
const next = [...lines];
|
|
965
|
+
let statsPatched = false;
|
|
966
|
+
for (let i = 0; i < lines.length; i++) {
|
|
967
|
+
const result = patchFooterStatsLine(lines[i], theme, opts);
|
|
968
|
+
if (!result.patched) continue;
|
|
969
|
+
next[i] = result.line;
|
|
970
|
+
statsPatched = true;
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
if (lines.length > 0) {
|
|
974
|
+
const pwd = patchFooterPwdLine(lines[0], theme, opts);
|
|
975
|
+
if (pwd !== lines[0]) next[0] = width > 0 ? truncateToWidth(pwd, width, theme.fg("dim", "...")) : pwd;
|
|
976
|
+
}
|
|
977
|
+
return { lines: next, patched: statsPatched };
|
|
978
|
+
}
|
|
979
|
+
|
|
666
980
|
/** 本插件视角下的模型对象子集(pi 的 Model 结构兼容,cost 只看四段扁平价) */
|
|
667
981
|
interface ModelLike {
|
|
668
982
|
provider: string;
|
|
@@ -759,144 +1073,93 @@ function computeFooterLabel(st: PricingState): FooterLabel | null {
|
|
|
759
1073
|
|
|
760
1074
|
function installCustomFooter(ctx: ExtensionContext): void {
|
|
761
1075
|
if (ctx.mode !== "tui") return; // 非 TUI(rpc/json)无 footer 可替换
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
1076
|
+
const Base = baseFooter;
|
|
1077
|
+
if (!Base) {
|
|
1078
|
+
// 拿不到内置组件 → 不覆盖,让 pi 自己渲染 footer(档位标签等增强本会话不生效)
|
|
1079
|
+
ctx.ui.setFooter(undefined);
|
|
1080
|
+
notifyOnce(
|
|
1081
|
+
ctx,
|
|
1082
|
+
`[pi-better-cost-display-footer] footer 增强未启用:${baseFooterError ?? "内置 footer 未加载"}`,
|
|
1083
|
+
);
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
1087
|
+
const base = new Base.FooterComponent(footerSessionView(ctx), footerData);
|
|
1088
|
+
// git 变更行数:启动查一次 + 10s 轮询 + 分支变化重查(抄 pi-cc-extensions)
|
|
1089
|
+
const refreshGit = () => refreshGitStats(ctx.cwd, () => tui.requestRender());
|
|
1090
|
+
const unsubBranch = footerData.onBranchChange(() => {
|
|
1091
|
+
refreshGit();
|
|
1092
|
+
tui.requestRender();
|
|
1093
|
+
});
|
|
1094
|
+
refreshGit();
|
|
1095
|
+
const gitTimer = setInterval(refreshGit, GIT_REFRESH_INTERVAL_MS);
|
|
1096
|
+
gitTimer.unref?.();
|
|
1097
|
+
return {
|
|
1098
|
+
invalidate() {},
|
|
1099
|
+
dispose() {
|
|
1100
|
+
clearInterval(gitTimer);
|
|
1101
|
+
unsubBranch?.();
|
|
1102
|
+
},
|
|
1103
|
+
render(width: number) {
|
|
1104
|
+
// 主题可能被运行时切换;ctx.ui.theme 是实时值,footer 工厂入参是装载时的快照
|
|
1105
|
+
const live = activeCtx?.ui.theme ?? theme;
|
|
1106
|
+
let lines: string[];
|
|
1107
|
+
try {
|
|
1108
|
+
lines = base.render(width);
|
|
1109
|
+
} catch (err) {
|
|
1110
|
+
disableFooter(ctx, err);
|
|
1111
|
+
return [];
|
|
781
1112
|
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
const autoIndicator = " (auto)"; // ponytail: 内置 auto-compact 关闭时此处仍显示
|
|
796
|
-
const contextPercentDisplay =
|
|
797
|
-
contextPercent === "?"
|
|
798
|
-
? `?/${formatTokens(contextWindow)}${autoIndicator}`
|
|
799
|
-
: `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;
|
|
800
|
-
let contextPercentStr: string;
|
|
801
|
-
if (contextPercentValue > 90) contextPercentStr = theme.fg("error", contextPercentDisplay);
|
|
802
|
-
else if (contextPercentValue > 70) contextPercentStr = theme.fg("warning", contextPercentDisplay);
|
|
803
|
-
else contextPercentStr = contextPercentDisplay;
|
|
804
|
-
// pwd + git 分支 + 会话名
|
|
805
|
-
let pwd = formatCwdForFooter(sm?.getCwd() ?? "", process.env.HOME || process.env.USERPROFILE);
|
|
806
|
-
const branch = footerData.getGitBranch();
|
|
807
|
-
if (branch) pwd = `${pwd} (${branch})`;
|
|
808
|
-
const sessionName = sm?.getSessionName();
|
|
809
|
-
if (sessionName) pwd = `${pwd} • ${sessionName}`;
|
|
810
|
-
// 统计段
|
|
811
|
-
const statsParts: string[] = [];
|
|
812
|
-
if (totals.input) statsParts.push(`↑${formatTokens(totals.input)}`);
|
|
813
|
-
if (totals.output) statsParts.push(`↓${formatTokens(totals.output)}`);
|
|
814
|
-
if (totals.cacheRead) statsParts.push(`R${formatTokens(totals.cacheRead)}`);
|
|
815
|
-
if (totals.cacheWrite) statsParts.push(`W${formatTokens(totals.cacheWrite)}`);
|
|
816
|
-
if ((totals.cacheRead > 0 || totals.cacheWrite > 0) && latestCacheHitRate !== undefined) {
|
|
817
|
-
const precision = currentCfg?.cacheHitRatePrecision ?? 1;
|
|
818
|
-
statsParts.push(`CH${latestCacheHitRate.toFixed(precision)}%`);
|
|
819
|
-
}
|
|
820
|
-
// tok/s:⚡ 常显;流式中为当前相位实时值(r 思考 / o 正文),空闲/工具执行时为定稿三档
|
|
821
|
-
const speedPart = formatSpeedPart();
|
|
822
|
-
if (speedPart) statsParts.push(speedPart);
|
|
823
|
-
// 订阅制 provider 无法从扩展读取 modelRuntime,退化为内置的特例
|
|
824
|
-
const usingSubscription = c?.model?.provider === "kimi-coding";
|
|
825
|
-
// 未配置模型只改 CH;金额符号和档位标签保持内置默认
|
|
826
|
-
const customPricing = getActiveModelPricing(currentCfg, c?.model);
|
|
827
|
-
const labelInfo = customPricing ? footerLabel : null;
|
|
828
|
-
// 漂移时标签转 warning 并加问号,金额前缀 ≈:宁可显示“可疑”,不静默显示错数字
|
|
829
|
-
const drift = customPricing ? footerDrift : false;
|
|
830
|
-
const labelText = labelInfo
|
|
831
|
-
? drift
|
|
832
|
-
? labelInfo.text.endsWith(")")
|
|
833
|
-
? `${labelInfo.text.slice(0, -1)}?)`
|
|
834
|
-
: `${labelInfo.text}?`
|
|
835
|
-
: labelInfo.text
|
|
836
|
-
: "";
|
|
837
|
-
const labelStr = labelInfo ? colorize(theme, drift ? "warning" : labelInfo.color, labelText) : "";
|
|
838
|
-
if (totals.cost || usingSubscription) {
|
|
839
|
-
const symbol = customPricing?.currencySymbol ?? "$";
|
|
840
|
-
const costStr = `${drift ? "≈" : ""}${symbol}${totals.cost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`;
|
|
841
|
-
statsParts.push(costStr + labelStr);
|
|
842
|
-
}
|
|
843
|
-
statsParts.push(contextPercentStr);
|
|
844
|
-
// 兜底:无计费段时标签置于统计段末尾
|
|
845
|
-
if (!(totals.cost || usingSubscription) && labelStr) statsParts.push(labelStr);
|
|
846
|
-
let statsLeft = statsParts.join(" ");
|
|
847
|
-
// 右侧:模型名 + thinking 档位 + 多 provider 前缀
|
|
848
|
-
const modelName = c?.model?.id || "no-model";
|
|
849
|
-
let statsLeftWidth = visibleWidth(statsLeft);
|
|
850
|
-
if (statsLeftWidth > width) {
|
|
851
|
-
statsLeft = truncateToWidth(statsLeft, width, "...");
|
|
852
|
-
statsLeftWidth = visibleWidth(statsLeft);
|
|
853
|
-
}
|
|
854
|
-
const minPadding = 2;
|
|
855
|
-
let rightSide: string = modelName;
|
|
856
|
-
if (c?.model?.reasoning) {
|
|
857
|
-
const thinkingLevel = c.thinkingLevel || "off";
|
|
858
|
-
rightSide =
|
|
859
|
-
thinkingLevel === "off" ? `${modelName} • thinking off` : `${modelName} • ${thinkingLevel}`;
|
|
860
|
-
}
|
|
861
|
-
if (footerData.getAvailableProviderCount() > 1 && c?.model) {
|
|
862
|
-
const withProvider = `(${c.model.provider}) ${rightSide}`;
|
|
863
|
-
if (statsLeftWidth + minPadding + visibleWidth(withProvider) <= width) rightSide = withProvider;
|
|
864
|
-
}
|
|
865
|
-
const rightSideWidth = visibleWidth(rightSide);
|
|
866
|
-
let statsLine: string;
|
|
867
|
-
if (statsLeftWidth + minPadding + rightSideWidth <= width) {
|
|
868
|
-
const padding = " ".repeat(width - statsLeftWidth - rightSideWidth);
|
|
869
|
-
statsLine = statsLeft + padding + rightSide;
|
|
870
|
-
} else {
|
|
871
|
-
const availableForRight = width - statsLeftWidth - minPadding;
|
|
872
|
-
if (availableForRight > 0) {
|
|
873
|
-
const truncatedRight = truncateToWidth(rightSide, availableForRight, "");
|
|
874
|
-
const padding = " ".repeat(Math.max(0, width - statsLeftWidth - visibleWidth(truncatedRight)));
|
|
875
|
-
statsLine = statsLeft + padding + truncatedRight;
|
|
876
|
-
} else {
|
|
877
|
-
statsLine = statsLeft;
|
|
1113
|
+
try {
|
|
1114
|
+
const result = patchFooterLines(lines, live, footerPatchOptions(), width);
|
|
1115
|
+
if (!result.patched && !footerPatchWarned) {
|
|
1116
|
+
// 内置布局变了(补丁打不上)→ 提醒一次,footer 本身仍由内置渲染
|
|
1117
|
+
footerPatchWarned = true;
|
|
1118
|
+
notifyOnce(
|
|
1119
|
+
ctx,
|
|
1120
|
+
"[pi-better-cost-display-footer] 内置 footer 布局已变,档位标签 / CH 精度等补丁未生效(其余显示正常)",
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
return result.lines;
|
|
1124
|
+
} catch {
|
|
1125
|
+
return lines; // 补丁出错 → 用内置原样,绝不让 footer 变空
|
|
878
1126
|
}
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1127
|
+
},
|
|
1128
|
+
};
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/** 内置组件渲染抛错:一次性退化为 pi 自带 footer,不在每帧重试 */
|
|
1133
|
+
function disableFooter(ctx: ExtensionContext, err: unknown): void {
|
|
1134
|
+
if (footerDisabled) return;
|
|
1135
|
+
footerDisabled = true;
|
|
1136
|
+
notifyOnce(
|
|
1137
|
+
ctx,
|
|
1138
|
+
`[pi-better-cost-display-footer] 复用内置 footer 失败,改用 pi 自带 footer:${
|
|
1139
|
+
err instanceof Error ? err.message : String(err)
|
|
1140
|
+
}`,
|
|
1141
|
+
);
|
|
1142
|
+
setTimeout(() => ctx.ui.setFooter(undefined), 0);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/** 当前帧的补丁内容(档位标签 / tok/s / 金额符号 / CH 精度) */
|
|
1146
|
+
function footerPatchOptions(): FooterPatchOptions {
|
|
1147
|
+
const model = activeCtx?.model;
|
|
1148
|
+
const customPricing = getActiveModelPricing(currentCfg, model);
|
|
1149
|
+
const drift = customPricing ? footerDrift : false;
|
|
1150
|
+
const info = customPricing ? footerLabel : null;
|
|
1151
|
+
const text = info ? (drift ? driftLabelText(info.text) : info.text) : undefined;
|
|
1152
|
+
return {
|
|
1153
|
+
symbol: customPricing?.currencySymbol,
|
|
1154
|
+
precision: currentCfg?.cacheHitRatePrecision,
|
|
1155
|
+
approx: drift,
|
|
1156
|
+
speed: formatSpeedPart(),
|
|
1157
|
+
label: info && text ? { text, color: drift ? "warning" : info.color } : undefined,
|
|
1158
|
+
contextBar: true,
|
|
1159
|
+
branchStats: gitStats ?? null, // 抄 cc:分支后跟 git 变更行数
|
|
1160
|
+
modelId: model?.id,
|
|
1161
|
+
thinkingLevel: activeCtx?.thinkingLevel,
|
|
1162
|
+
};
|
|
900
1163
|
}
|
|
901
1164
|
|
|
902
1165
|
function apply(pi: ExtensionAPI, ctx: ExtensionContext): void {
|
|
@@ -1027,6 +1290,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1027
1290
|
`守卫: ${appliedKey ? "已置位(配置与档位未变则不重注册)" : "未置位(下一轮重注册)"}`,
|
|
1028
1291
|
`漂移重试: ${driftRetries}/${DRIFT_RETRY_LIMIT}`,
|
|
1029
1292
|
`速率折算: 思考 ${speedRatios().r.toFixed(2)} / 正文 ${speedRatios().o.toFixed(2)} 字符/token(真实 token 校准,初始 4)`,
|
|
1293
|
+
`footer: ${baseFooterStatusText()}`,
|
|
1030
1294
|
`最近注册: ${lastRegisterResult}`,
|
|
1031
1295
|
];
|
|
1032
1296
|
const text = `[pi-better-cost-display-footer]\n${lines.join("\n")}`;
|
|
@@ -1034,8 +1298,13 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1034
1298
|
else console.log(text);
|
|
1035
1299
|
},
|
|
1036
1300
|
});
|
|
1037
|
-
pi.on("session_start", (_e, ctx) => {
|
|
1301
|
+
pi.on("session_start", async (_e, ctx) => {
|
|
1038
1302
|
resetSpeedSession();
|
|
1303
|
+
footerPatchWarned = false;
|
|
1304
|
+
footerDisabled = false;
|
|
1305
|
+
gitStats = undefined; // 会话可能换了仓库
|
|
1306
|
+
// 加载 pi 自己的 footer 组件:拿不到就不覆盖(用 pi 自带 footer),不当“旧版复刻”卡着新 footer
|
|
1307
|
+
await loadBaseFooter();
|
|
1039
1308
|
installCustomFooter(ctx);
|
|
1040
1309
|
apply(pi, ctx);
|
|
1041
1310
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yalieny/pi-better-cost-display-footer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "pi extension: scriptable peak/off-peak tier rules per provider with an enhanced footer (tier label, cache-hit-rate precision, custom currency symbol)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"type": "module",
|
|
11
11
|
"scripts": {
|
|
12
|
-
"test": "node --test test/tier.test.ts test/speed.test.ts"
|
|
12
|
+
"test": "node --test test/tier.test.ts test/speed.test.ts test/footer-patch.test.ts"
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
15
|
"@earendil-works/pi-coding-agent": "*",
|