pi-cc-extensions 0.8.70 → 0.8.72
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.en.md +3 -1
- package/README.md +3 -1
- package/extensions/config/config.ts +4 -0
- package/extensions/config/panel.ts +16 -0
- package/extensions/feature/shell/footer.ts +413 -0
- package/extensions/index.ts +2 -0
- package/extensions/renderer/compact-mode.ts +154 -9
- package/extensions/renderer/default-mode.ts +82 -0
- package/extensions/renderer/mouse/interaction.ts +11 -6
- package/extensions/renderer/mouse/packets.ts +1 -0
- package/extensions/renderer/mouse/scroll.ts +23 -1
- package/extensions/renderer/tool/diff/diff-edit-render.ts +27 -7
- package/extensions/renderer/tool/diff/diff-inline.ts +13 -1
- package/extensions/renderer/tool/diff/diff-layout.ts +37 -5
- package/extensions/renderer/tool/diff/diff-parse.ts +18 -4
- package/extensions/renderer/tool/grouping.ts +49 -8
- package/extensions/renderer/tool/result.ts +6 -2
- package/extensions/utils/patch-keys.ts +1 -0
- package/extensions/utils/sgr-mouse.ts +5 -0
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -43,6 +43,7 @@ Run `/reload` after installation.
|
|
|
43
43
|
| Settings panel | `Style / Diff / Thinking / UI / Feature` tabs | `/ccstyle` |
|
|
44
44
|
| Context inspection | Usage breakdown and previews for the system prompt, memory, skills, tools definition, and messages | `/context` |
|
|
45
45
|
| Session/Subagent references | Search and inject effective context from previous Sessions or existing SubAgents | `@` |
|
|
46
|
+
| Status bar | Two-line chips: model, context, cache, cost, git; requires a Nerd Font | `/ccstyle` Feature |
|
|
46
47
|
| Theme | Bundled CC Dark and CC Light themes | `/theme` |
|
|
47
48
|
|
|
48
49
|
## Configuration
|
|
@@ -82,7 +83,8 @@ Run `/reload` after installation.
|
|
|
82
83
|
"enableContextCommand": true, // /context usage check
|
|
83
84
|
"enableAgentSummary": true, // per-turn tool summary
|
|
84
85
|
"enableWorkingMessage": true, // Working... bottom token/elapsed
|
|
85
|
-
"enableAliases": true
|
|
86
|
+
"enableAliases": true, // /clear, /exit aliases
|
|
87
|
+
"enableCustomFooter": true // two-line chips status bar (needs a Nerd Font)
|
|
86
88
|
}
|
|
87
89
|
```
|
|
88
90
|
|
package/README.md
CHANGED
|
@@ -43,6 +43,7 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
43
43
|
| 配置面板 | `Style / Diff / Thinking / UI / Feature` 五页签 | `/ccstyle` |
|
|
44
44
|
| 上下文检查 | 查看上下文占用,并预览 System prompt、Memory、Skills、Tools definition 和消息内容 | `/context` |
|
|
45
45
|
| Session/Subagent 引用 | 搜索并注入历史 Session 或现有 SubAgent 的有效上下文 | `@` |
|
|
46
|
+
| 状态栏 | 两行 chips:模型、上下文、缓存、费用、git;需要 Nerd Font | `/ccstyle` Feature |
|
|
46
47
|
| 主题 | 随包提供内置 CC Dark、CC Light 主题 | `/theme` |
|
|
47
48
|
|
|
48
49
|
## 配置
|
|
@@ -82,7 +83,8 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
82
83
|
"enableContextCommand": true, // /context 上下文检查
|
|
83
84
|
"enableAgentSummary": true, // 每回合工具摘要
|
|
84
85
|
"enableWorkingMessage": true, // Working... 底部 token/耗时
|
|
85
|
-
"enableAliases": true
|
|
86
|
+
"enableAliases": true, // /clear、/exit 别名
|
|
87
|
+
"enableCustomFooter": true // 两行 chips 状态栏(需 Nerd Font)
|
|
86
88
|
}
|
|
87
89
|
```
|
|
88
90
|
|
|
@@ -64,6 +64,7 @@ export type Config = {
|
|
|
64
64
|
enableAgentSummary: boolean;
|
|
65
65
|
enableWorkingMessage: boolean;
|
|
66
66
|
enableAliases: boolean;
|
|
67
|
+
enableCustomFooter: boolean;
|
|
67
68
|
};
|
|
68
69
|
|
|
69
70
|
const AGENT_DIR = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
@@ -125,6 +126,7 @@ export const DEFAULT_CONFIG: Config = {
|
|
|
125
126
|
enableAgentSummary: true,
|
|
126
127
|
enableWorkingMessage: true,
|
|
127
128
|
enableAliases: true,
|
|
129
|
+
enableCustomFooter: true,
|
|
128
130
|
};
|
|
129
131
|
|
|
130
132
|
function pickEnum<T extends string>(value: unknown, allowed: readonly T[], fallback: T): T {
|
|
@@ -223,6 +225,7 @@ export function normalizeConfig(input: unknown): Config {
|
|
|
223
225
|
enableAgentSummary: source.enableAgentSummary !== false,
|
|
224
226
|
enableWorkingMessage: source.enableWorkingMessage !== false,
|
|
225
227
|
enableAliases: source.enableAliases !== false,
|
|
228
|
+
enableCustomFooter: source.enableCustomFooter !== false,
|
|
226
229
|
};
|
|
227
230
|
}
|
|
228
231
|
|
|
@@ -276,6 +279,7 @@ export function formatConfigStatus(source: Config = config): string {
|
|
|
276
279
|
`agentSummary=${source.enableAgentSummary ? "on" : "off"}`,
|
|
277
280
|
`workingMsg=${source.enableWorkingMessage ? "on" : "off"}`,
|
|
278
281
|
`aliases=${source.enableAliases ? "on" : "off"}`,
|
|
282
|
+
`footer=${source.enableCustomFooter ? "on" : "off"}`,
|
|
279
283
|
].join(" · ");
|
|
280
284
|
}
|
|
281
285
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { Input, SettingsList, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
9
9
|
import type { CompactThinkingController } from "../feature/compact-thinking.ts";
|
|
10
|
+
import { applyCustomFooter } from "../feature/shell/footer.ts";
|
|
10
11
|
import { applyStartupHeader } from "../feature/shell/startup-header.ts";
|
|
11
12
|
import type { ToolGroupingHooks } from "../renderer/tool/grouping.ts";
|
|
12
13
|
import {
|
|
@@ -455,6 +456,13 @@ export async function showCcstylePanel(
|
|
|
455
456
|
"Aliases disabled.",
|
|
456
457
|
config.enableAliases,
|
|
457
458
|
);
|
|
459
|
+
const customFooterToggle = featureToggleSetting(
|
|
460
|
+
"enableCustomFooter",
|
|
461
|
+
"Status bar",
|
|
462
|
+
"Two-line chips status bar with model, context, git, and usage. Applies immediately. Needs a Nerd Font.",
|
|
463
|
+
"Pi native footer restored.",
|
|
464
|
+
config.enableCustomFooter,
|
|
465
|
+
);
|
|
458
466
|
const featureToggles: Record<string, { apply: (on: boolean) => void }> = {
|
|
459
467
|
enableSessionReference: sessionReferenceToggle,
|
|
460
468
|
enableSubagentAutocomplete: subagentAutocompleteToggle,
|
|
@@ -465,6 +473,13 @@ export async function showCcstylePanel(
|
|
|
465
473
|
};
|
|
466
474
|
|
|
467
475
|
const onSettingChange = (id: string, value: string) => {
|
|
476
|
+
if (id === "enableCustomFooter") {
|
|
477
|
+
updateConfig({ enableCustomFooter: value === "on" });
|
|
478
|
+
customFooterToggle.apply(value === "on");
|
|
479
|
+
applyCustomFooter(ctx);
|
|
480
|
+
ctx.ui.notify(`Updated ${id}: ${value}`, "info");
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
468
483
|
// 额外功能开关:字段名与配置布尔字段一一对应,切换后重启生效。
|
|
469
484
|
const featureToggle = featureToggles[id];
|
|
470
485
|
if (featureToggle) {
|
|
@@ -658,6 +673,7 @@ export async function showCcstylePanel(
|
|
|
658
673
|
agentSummaryToggle.setting,
|
|
659
674
|
workingMessageToggle.setting,
|
|
660
675
|
aliasesToggle.setting,
|
|
676
|
+
customFooterToggle.setting,
|
|
661
677
|
],
|
|
662
678
|
},
|
|
663
679
|
];
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 自定义底栏:chips + zentui 图标/句子
|
|
3
|
+
*
|
|
4
|
+
* 第一行:model thinking · ██████░░░░ 45%/200k · 42% · $0.01 · 用量
|
|
5
|
+
* 第二行:cwd in session on branch (+16 −1) · 其他扩展状态
|
|
6
|
+
*
|
|
7
|
+
* - line1 短芯片,· 分隔;缓存用 zentui ,费用 success
|
|
8
|
+
* - line2 用 zentui 句式 in / on +
|
|
9
|
+
* - 当前模型用量:优先 pi-usage 的 usage 状态;xAI 等不写 statusline 的供应商由本扩展补拉
|
|
10
|
+
*
|
|
11
|
+
* 模型/计费/思考级别变化时自动更新(pi.on 全局事件 + render 实时计算)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
16
|
+
import { execFile } from "node:child_process";
|
|
17
|
+
import { config } from "../../config/config.ts";
|
|
18
|
+
import { stripAnsi } from "../../utils/ansi-text.ts";
|
|
19
|
+
|
|
20
|
+
const GIT_REFRESH_INTERVAL_MS = 10_000;
|
|
21
|
+
const USAGE_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
|
|
22
|
+
const USAGE_TIMEOUT_MS = 15_000;
|
|
23
|
+
// zentui NERD_DEFAULT_ICONS:git / cacheHit
|
|
24
|
+
const ICON_GIT = "";
|
|
25
|
+
const ICON_CACHE = "";
|
|
26
|
+
|
|
27
|
+
type GitStats = { add: number; del: number };
|
|
28
|
+
|
|
29
|
+
type XaiFooterReport = {
|
|
30
|
+
buckets?: Array<{
|
|
31
|
+
id?: string;
|
|
32
|
+
unit?: string;
|
|
33
|
+
used?: number;
|
|
34
|
+
limit?: number;
|
|
35
|
+
remaining?: number;
|
|
36
|
+
}>;
|
|
37
|
+
metrics?: Array<{ id?: string; value?: unknown }>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function parseGitStats(stdout: string): GitStats {
|
|
41
|
+
let add = 0;
|
|
42
|
+
let del = 0;
|
|
43
|
+
for (const line of stdout.split("\n")) {
|
|
44
|
+
const match = line.match(/^(\d+)\s+(\d+)/);
|
|
45
|
+
if (match) {
|
|
46
|
+
add += Number(match[1]);
|
|
47
|
+
del += Number(match[2]);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { add, del };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function classifyStatus(key: string): "usage" | "skip" | "other" {
|
|
54
|
+
if (key === "model") return "skip"; // line1 已有供应商/模型
|
|
55
|
+
if (/usage|quota|balance/i.test(key)) return "usage";
|
|
56
|
+
return "other";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function formatXaiFooterChip(report: XaiFooterReport): string | undefined {
|
|
60
|
+
const included = report.buckets?.find((b) => b.id === "included-allowance");
|
|
61
|
+
if (included?.unit === "percent" && typeof included.used === "number") {
|
|
62
|
+
return `xAI ${Math.round(included.used)}%`;
|
|
63
|
+
}
|
|
64
|
+
if (included && included.unit !== "percent") {
|
|
65
|
+
if (
|
|
66
|
+
typeof included.used === "number" &&
|
|
67
|
+
typeof included.limit === "number" &&
|
|
68
|
+
included.limit > 0
|
|
69
|
+
) {
|
|
70
|
+
return `xAI ${Math.round((included.used / included.limit) * 100)}%`;
|
|
71
|
+
}
|
|
72
|
+
if (typeof included.remaining === "number") {
|
|
73
|
+
return `xAI $${included.remaining.toFixed(2)}`;
|
|
74
|
+
}
|
|
75
|
+
if (typeof included.used === "number") {
|
|
76
|
+
return `xAI $${included.used.toFixed(2)}`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const prepaid = report.metrics?.find((m) => m.id === "prepaid-balance");
|
|
80
|
+
if (typeof prepaid?.value === "number") {
|
|
81
|
+
return `xAI $${prepaid.value.toFixed(2)}`;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function pickFooterUsageText(
|
|
87
|
+
pluginUsage: string | undefined,
|
|
88
|
+
localChip: string | undefined,
|
|
89
|
+
): string | undefined {
|
|
90
|
+
const plugin = pluginUsage?.trim();
|
|
91
|
+
if (plugin && !/^checking$/i.test(plugin)) return plugin;
|
|
92
|
+
const local = localChip?.trim();
|
|
93
|
+
return local || undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let piUsageMod: any | null | undefined;
|
|
97
|
+
|
|
98
|
+
async function getPiUsage(): Promise<any | null> {
|
|
99
|
+
if (piUsageMod !== undefined) return piUsageMod;
|
|
100
|
+
try {
|
|
101
|
+
const spec = "@narumitw/pi-usage";
|
|
102
|
+
piUsageMod = await import(spec);
|
|
103
|
+
} catch {
|
|
104
|
+
piUsageMod = null;
|
|
105
|
+
}
|
|
106
|
+
return piUsageMod;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function colorUsageChip(theme: any, text: string): string {
|
|
110
|
+
return theme.fg("dim", stripAnsi(text));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let currentTui: any = undefined;
|
|
114
|
+
let refreshCurrentGitStats: (() => void) | undefined;
|
|
115
|
+
let refreshCurrentUsage: (() => void) | undefined;
|
|
116
|
+
|
|
117
|
+
const createCustomFooterFactory =
|
|
118
|
+
(ctx: ExtensionContext) => (tui: any, theme: any, footerData: any) => {
|
|
119
|
+
currentTui = tui;
|
|
120
|
+
const sep = theme.fg("muted", " · ");
|
|
121
|
+
const joinChips = (parts: string[]) => parts.filter(Boolean).join(sep);
|
|
122
|
+
let gitStats: GitStats | undefined;
|
|
123
|
+
let gitRefreshRunning = false;
|
|
124
|
+
let localUsageChip = "";
|
|
125
|
+
let usageGeneration = 0;
|
|
126
|
+
let usageAbort: AbortController | undefined;
|
|
127
|
+
let disposed = false;
|
|
128
|
+
|
|
129
|
+
// 查询在 render 外执行,保留旧值;仅统计结果真正变化时重绘,避免周期性清零抖动。
|
|
130
|
+
const refreshGitStats = () => {
|
|
131
|
+
if (gitRefreshRunning || disposed) return;
|
|
132
|
+
gitRefreshRunning = true;
|
|
133
|
+
execFile(
|
|
134
|
+
"git",
|
|
135
|
+
["diff", "--numstat", "HEAD"],
|
|
136
|
+
{ cwd: ctx.cwd, timeout: 2000 },
|
|
137
|
+
(err, stdout) => {
|
|
138
|
+
gitRefreshRunning = false;
|
|
139
|
+
if (err || disposed) return;
|
|
140
|
+
const next = parseGitStats(stdout);
|
|
141
|
+
if (gitStats?.add === next.add && gitStats.del === next.del) return;
|
|
142
|
+
gitStats = next;
|
|
143
|
+
tui.requestRender();
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
refreshCurrentGitStats = refreshGitStats;
|
|
148
|
+
refreshGitStats();
|
|
149
|
+
const gitRefreshTimer = setInterval(refreshGitStats, GIT_REFRESH_INTERVAL_MS);
|
|
150
|
+
gitRefreshTimer.unref?.();
|
|
151
|
+
|
|
152
|
+
const refreshUsage = () => {
|
|
153
|
+
if (disposed) return;
|
|
154
|
+
const generation = ++usageGeneration;
|
|
155
|
+
usageAbort?.abort();
|
|
156
|
+
const controller = new AbortController();
|
|
157
|
+
usageAbort = controller;
|
|
158
|
+
const stale = () => disposed || generation !== usageGeneration || controller.signal.aborted;
|
|
159
|
+
void (async () => {
|
|
160
|
+
try {
|
|
161
|
+
const model = ctx.model;
|
|
162
|
+
const api = await getPiUsage();
|
|
163
|
+
if (stale()) return;
|
|
164
|
+
if (!api || !model) {
|
|
165
|
+
if (localUsageChip) {
|
|
166
|
+
localUsageChip = "";
|
|
167
|
+
tui.requestRender();
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const adapter = api.adapterForProvider?.(model.provider);
|
|
172
|
+
if (!adapter || adapter.publishesStatusline !== false) {
|
|
173
|
+
if (localUsageChip) {
|
|
174
|
+
localUsageChip = "";
|
|
175
|
+
tui.requestRender();
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const auth = await api.resolveUsageAuth(ctx, adapter);
|
|
180
|
+
if (stale()) return;
|
|
181
|
+
if (!auth) {
|
|
182
|
+
const next = `${adapter.displayName} ✗`;
|
|
183
|
+
if (localUsageChip !== next) {
|
|
184
|
+
localUsageChip = next;
|
|
185
|
+
tui.requestRender();
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const guard = async () => {
|
|
190
|
+
if (stale()) throw new Error("aborted");
|
|
191
|
+
const again = await api.resolveUsageAuth(ctx, adapter);
|
|
192
|
+
if (again?.fingerprint !== auth.fingerprint) {
|
|
193
|
+
throw new Error("aborted");
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
const report = await api.queryProviderUsage(
|
|
197
|
+
adapter,
|
|
198
|
+
auth,
|
|
199
|
+
controller.signal,
|
|
200
|
+
USAGE_TIMEOUT_MS,
|
|
201
|
+
guard,
|
|
202
|
+
);
|
|
203
|
+
if (stale()) return;
|
|
204
|
+
const next =
|
|
205
|
+
(adapter.id === "xai" ? formatXaiFooterChip(report) : undefined) ||
|
|
206
|
+
api.formatUsageStatusline?.(report, model) ||
|
|
207
|
+
`${adapter.displayName} ✗`;
|
|
208
|
+
if (localUsageChip !== next) {
|
|
209
|
+
localUsageChip = next;
|
|
210
|
+
tui.requestRender();
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
if (stale()) return;
|
|
214
|
+
const name = ctx.model?.provider === "xai" ? "xAI" : "usage";
|
|
215
|
+
const next = `${name} ✗`;
|
|
216
|
+
if (localUsageChip !== next) {
|
|
217
|
+
localUsageChip = next;
|
|
218
|
+
tui.requestRender();
|
|
219
|
+
}
|
|
220
|
+
} finally {
|
|
221
|
+
if (usageAbort === controller) usageAbort = undefined;
|
|
222
|
+
}
|
|
223
|
+
})();
|
|
224
|
+
};
|
|
225
|
+
refreshCurrentUsage = refreshUsage;
|
|
226
|
+
refreshUsage();
|
|
227
|
+
const usageRefreshTimer = setInterval(refreshUsage, USAGE_REFRESH_INTERVAL_MS);
|
|
228
|
+
usageRefreshTimer.unref?.();
|
|
229
|
+
|
|
230
|
+
// 分支变化时同步更新分支名和相对 HEAD 的统计。
|
|
231
|
+
const unsubBranch = footerData.onBranchChange(() => {
|
|
232
|
+
refreshGitStats();
|
|
233
|
+
tui.requestRender();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const fmt = (n: number): string => {
|
|
237
|
+
if (n < 1000) return `${n}`;
|
|
238
|
+
if (n < 10000) return `${(n / 1000).toFixed(1)}k`;
|
|
239
|
+
if (n < 1000000) return `${Math.round(n / 1000)}k`;
|
|
240
|
+
if (n < 10000000) return `${(n / 1000000).toFixed(1)}M`;
|
|
241
|
+
return `${Math.round(n / 1000000)}M`;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// ---- pi-cc-status 风格:上下文条形图 ----
|
|
245
|
+
const BAR_WIDTH = 10;
|
|
246
|
+
const BAR_WARNING = 80; // >= 80% 转 warning 色
|
|
247
|
+
const BAR_ERROR = 95; // >= 95% 转 error 色
|
|
248
|
+
const barGauge = (pct: number): string => {
|
|
249
|
+
const clamped = Math.max(0, Math.min(100, pct));
|
|
250
|
+
const filled = Math.round((clamped / 100) * BAR_WIDTH);
|
|
251
|
+
const color = clamped >= BAR_ERROR ? "error" : clamped >= BAR_WARNING ? "warning" : "accent";
|
|
252
|
+
return theme.fg(color, "█".repeat(filled)) + theme.fg("dim", "░".repeat(BAR_WIDTH - filled));
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// pi-cc-status showCachePercent:最近一条助手消息的 cacheRead 占比
|
|
256
|
+
const getCachePct = (): number => {
|
|
257
|
+
const branch = ctx.sessionManager.getBranch();
|
|
258
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
259
|
+
const e = branch[i];
|
|
260
|
+
if (e.type !== "message" || e.message.role !== "assistant") continue;
|
|
261
|
+
const u = e.message.usage;
|
|
262
|
+
if (!u) return 0;
|
|
263
|
+
const total = u.input + u.cacheWrite + u.cacheRead;
|
|
264
|
+
return total > 0 ? (u.cacheRead * 100) / total : 0;
|
|
265
|
+
}
|
|
266
|
+
return 0;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const render = (width: number): string[] => {
|
|
270
|
+
const model = ctx.model;
|
|
271
|
+
const contextUsage = ctx.getContextUsage();
|
|
272
|
+
const contextWindow = contextUsage?.contextWindow ?? model?.contextWindow ?? 0;
|
|
273
|
+
const percent = contextUsage?.percent;
|
|
274
|
+
|
|
275
|
+
let cost = 0;
|
|
276
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
277
|
+
if (entry.type === "message") {
|
|
278
|
+
const m = entry.message;
|
|
279
|
+
if (m.role === "assistant") cost += m.usage?.cost?.total || 0;
|
|
280
|
+
else if (m.role === "toolResult" && m.usage) cost += m.usage.cost?.total || 0;
|
|
281
|
+
} else if (
|
|
282
|
+
(entry.type === "branch_summary" || entry.type === "compaction") &&
|
|
283
|
+
entry.usage
|
|
284
|
+
) {
|
|
285
|
+
cost += entry.usage.cost?.total || 0;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// cwd 默认只显示最底层路径(根目录时回退完整路径)
|
|
290
|
+
const cwdLabel = ctx.cwd.split(/[\\/]/).filter(Boolean).pop() || ctx.cwd;
|
|
291
|
+
|
|
292
|
+
const usingSubscription =
|
|
293
|
+
!!model &&
|
|
294
|
+
["kimi-coding", "xai", "github-copilot", "openai"].includes(model.provider || "");
|
|
295
|
+
const modelLabel = model
|
|
296
|
+
? model.id.includes("/")
|
|
297
|
+
? model.id
|
|
298
|
+
: `${model.provider}/${model.id}`
|
|
299
|
+
: "no-model";
|
|
300
|
+
|
|
301
|
+
const thinkingLevelStr = ctx.thinkingLevel || "off";
|
|
302
|
+
const thinkingColor = theme.getThinkingBorderColor(thinkingLevelStr);
|
|
303
|
+
|
|
304
|
+
const otherStatuses: string[] = [];
|
|
305
|
+
const usageStatuses: string[] = [];
|
|
306
|
+
for (const [key, raw] of Array.from(
|
|
307
|
+
footerData.getExtensionStatuses().entries() as Iterable<[string, string]>,
|
|
308
|
+
).sort(([a], [b]) => a.localeCompare(b))) {
|
|
309
|
+
const t = raw.replace(/[\r\n\t]+/g, " ").trim();
|
|
310
|
+
if (!t) continue;
|
|
311
|
+
const kind = classifyStatus(key);
|
|
312
|
+
if (kind === "skip") continue;
|
|
313
|
+
if (kind === "usage") {
|
|
314
|
+
usageStatuses.push(stripAnsi(t));
|
|
315
|
+
} else {
|
|
316
|
+
otherStatuses.push(theme.fg("dim", stripAnsi(t)));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const usageText = pickFooterUsageText(usageStatuses[0], localUsageChip);
|
|
321
|
+
|
|
322
|
+
const pctLabel = percent === null || percent === undefined ? "?" : `${Math.floor(percent)}%`;
|
|
323
|
+
const cachePct = getCachePct();
|
|
324
|
+
const costChip =
|
|
325
|
+
cost || usingSubscription
|
|
326
|
+
? theme.fg("dim", `$${cost.toFixed(2)}`) +
|
|
327
|
+
(usingSubscription ? theme.fg("warning", " sub") : "")
|
|
328
|
+
: "";
|
|
329
|
+
const line1 = joinChips([
|
|
330
|
+
theme.fg("accent", modelLabel),
|
|
331
|
+
model?.reasoning ? thinkingColor(thinkingLevelStr) : "",
|
|
332
|
+
barGauge(percent ?? 0) + theme.fg("dim", ` ${pctLabel}/${fmt(contextWindow)}`),
|
|
333
|
+
cachePct > 0 ? theme.fg("dim", `${ICON_CACHE} ${Math.floor(cachePct)}%`) : "",
|
|
334
|
+
costChip,
|
|
335
|
+
usageText ? colorUsageChip(theme, usageText) : "",
|
|
336
|
+
]);
|
|
337
|
+
|
|
338
|
+
const gitColor = theme.getThinkingBorderColor("medium");
|
|
339
|
+
let place = theme.fg("accent", cwdLabel);
|
|
340
|
+
const sessionName = ctx.sessionManager.getSessionName();
|
|
341
|
+
if (sessionName) {
|
|
342
|
+
place += theme.fg("muted", " in ") + theme.fg("success", sessionName);
|
|
343
|
+
}
|
|
344
|
+
const branch = footerData.getGitBranch();
|
|
345
|
+
if (branch) {
|
|
346
|
+
const stats =
|
|
347
|
+
gitStats && (gitStats.add || gitStats.del)
|
|
348
|
+
? theme.fg("dim", " (") +
|
|
349
|
+
theme.fg("success", `+${gitStats.add}`) +
|
|
350
|
+
" " +
|
|
351
|
+
theme.fg("error", `−${gitStats.del}`) +
|
|
352
|
+
theme.fg("dim", ")")
|
|
353
|
+
: "";
|
|
354
|
+
place += theme.fg("muted", " on ") + gitColor(`${ICON_GIT} ${branch}`) + stats;
|
|
355
|
+
}
|
|
356
|
+
const line2 = joinChips([place, ...otherStatuses]);
|
|
357
|
+
|
|
358
|
+
return [truncateToWidth(line1, width), ...(line2 ? [truncateToWidth(line2, width)] : [])];
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
render,
|
|
363
|
+
invalidate(): void {
|
|
364
|
+
tui.requestRender();
|
|
365
|
+
},
|
|
366
|
+
dispose(): void {
|
|
367
|
+
disposed = true;
|
|
368
|
+
usageAbort?.abort();
|
|
369
|
+
usageAbort = undefined;
|
|
370
|
+
clearInterval(gitRefreshTimer);
|
|
371
|
+
clearInterval(usageRefreshTimer);
|
|
372
|
+
unsubBranch();
|
|
373
|
+
if (currentTui === tui) {
|
|
374
|
+
currentTui = undefined;
|
|
375
|
+
refreshCurrentGitStats = undefined;
|
|
376
|
+
refreshCurrentUsage = undefined;
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* 按配置应用底栏:on → 自定义 footer;off → 恢复官方默认 footer。
|
|
384
|
+
* 导出供 /ccstyle 面板在切换开关时实时重应用。
|
|
385
|
+
*/
|
|
386
|
+
export function applyCustomFooter(ctx: ExtensionContext): void {
|
|
387
|
+
if (!ctx?.hasUI || typeof ctx.ui?.setFooter !== "function") return;
|
|
388
|
+
if (!config.enableCustomFooter) {
|
|
389
|
+
ctx.ui.setFooter(undefined);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
ctx.ui.setFooter(createCustomFooterFactory(ctx));
|
|
394
|
+
} catch (err) {
|
|
395
|
+
ctx.ui.notify(`footer error: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export default function (pi: ExtensionAPI) {
|
|
400
|
+
// 模型/思考级别变化时强制重渲染(自定义 footer 不会被内置 invalidate() 触达)
|
|
401
|
+
pi.on("model_select", () => {
|
|
402
|
+
currentTui?.requestRender();
|
|
403
|
+
refreshCurrentUsage?.();
|
|
404
|
+
});
|
|
405
|
+
pi.on("thinking_level_select", () => currentTui?.requestRender());
|
|
406
|
+
// 工具执行完成后立即刷新;定时器只负责兜底捕获外部文件变化。
|
|
407
|
+
pi.on("tool_execution_end", () => refreshCurrentGitStats?.());
|
|
408
|
+
|
|
409
|
+
// 启动 / /reload / 新建会话 时按配置恢复
|
|
410
|
+
pi.on("session_start", (_event, ctx) => {
|
|
411
|
+
applyCustomFooter(ctx);
|
|
412
|
+
});
|
|
413
|
+
}
|
package/extensions/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { config } from "./config/config.ts";
|
|
|
4
4
|
// shell
|
|
5
5
|
import piAliases from "./feature/shell/aliases.ts";
|
|
6
6
|
import { installFlushDockedBash } from "./feature/shell/flush-docked-bash.ts";
|
|
7
|
+
import customFooter from "./feature/shell/footer.ts";
|
|
7
8
|
import piStartupHeader from "./feature/shell/startup-header.ts";
|
|
8
9
|
import workingMessage from "./feature/shell/working-message.ts";
|
|
9
10
|
|
|
@@ -24,6 +25,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
24
25
|
installFlushDockedBash();
|
|
25
26
|
piStartupHeader(pi);
|
|
26
27
|
if (config.enableWorkingMessage) workingMessage(pi);
|
|
28
|
+
customFooter(pi);
|
|
27
29
|
|
|
28
30
|
// render stack:thinking controller 直接交给 style 作 query
|
|
29
31
|
markdownEnhance(pi);
|