pi-cc-extensions 1.0.12 → 1.0.13
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 +5 -3
- package/README.md +5 -3
- package/extensions/config/config.ts +20 -0
- package/extensions/config/panel.ts +61 -6
- package/extensions/feature/agent-summary/core.ts +11 -16
- package/extensions/feature/agent-summary/index.ts +60 -11
- package/extensions/feature/compact-thinking.ts +29 -11
- package/extensions/feature/shell/footer-layout.ts +54 -6
- package/extensions/feature/shell/footer.ts +23 -5
- package/extensions/feature/shell/session-tokens.ts +52 -0
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -33,7 +33,7 @@ Run `/reload` after installation.
|
|
|
33
33
|
| Settings panel | `Style / Features / UI / Diff / Thinking / Footer` tabs | `/ccstyle` |
|
|
34
34
|
| Context inspection | Usage breakdown and previews for the system prompt, memory, skills, tools definition, and messages | `/context` |
|
|
35
35
|
| Session/Subagent references | Search and inject effective context from previous Sessions or existing SubAgents | `@` |
|
|
36
|
-
| Status bar | Shows model, context, cache, cost, and git; also works with `@narumitw/pi-usage` for live quota. Git/cache Nerd Font icons can be turned off | `/ccstyle` |
|
|
36
|
+
| Status bar | Shows model, context, cache, cost, the conversation token total, and git; also works with `@narumitw/pi-usage` for live quota. Git/cache Nerd Font icons can be turned off | `/ccstyle` |
|
|
37
37
|
| Theme | Bundled CC Dark and CC Light themes | `/settings` → Theme |
|
|
38
38
|
|
|
39
39
|
Renderer snapshots for `on` / `compact` modes: [default](./docs/tool-render-examples-default.md) · [compact](./docs/tool-render-examples-compact.md)
|
|
@@ -77,7 +77,8 @@ Renderer snapshots for `on` / `compact` modes: [default](./docs/tool-render-exam
|
|
|
77
77
|
|
|
78
78
|
// thinking
|
|
79
79
|
"useSummaryTitlesAsThinkingTitle": true, // use latest summary as thinking title
|
|
80
|
-
"previewLines": 3, // preview lines; 0 hides
|
|
80
|
+
"previewLines": 3, // preview lines while thinking; 0 hides
|
|
81
|
+
"completedPreviewLines": null, // completed preview lines; null follows previewLines, 0 hides. Panel accepts any number or follow
|
|
81
82
|
"animationIntervalMs": 90, // title animation interval (ms)
|
|
82
83
|
|
|
83
84
|
// footer
|
|
@@ -85,12 +86,13 @@ Renderer snapshots for `on` / `compact` modes: [default](./docs/tool-render-exam
|
|
|
85
86
|
"footerTokenRate": true, // output rate chip; false hides it
|
|
86
87
|
"footerTokenRateMode": "average", // average: whole-prompt avg; live: trailing 1s rate
|
|
87
88
|
"footerTokenRateTtft": true, // append TTFT to the rate chip; false shows tok/s only
|
|
89
|
+
"footerSessionTokens": false, // conversation token total chip (∑); sums usage.totalTokens from session entries. Off by default
|
|
88
90
|
"footerCurrency": "USD", // display currency for the footer cost
|
|
89
91
|
"footerCurrencyRate": null, // display units per 1 cost unit; null changes the symbol only
|
|
90
92
|
"footerNerdIcons": true, // Nerd Font glyphs for git/cache; false = plain text
|
|
91
93
|
"footerGitStatsMode": "working", // working (default): uncommitted; branch: cumulative net changes
|
|
92
94
|
"footerHiddenKeys": [], // hidden chip keys
|
|
93
|
-
"footerLine1Keys": ["model", "context", "cache", "rate", "cost", "pi-usage"], // chip order per line; builtins and plugins share one layout
|
|
95
|
+
"footerLine1Keys": ["model", "context", "cache", "rate", "cost", "tokens", "pi-usage"], // chip order per line; builtins and plugins share one layout
|
|
94
96
|
"footerLine2Keys": ["cwd", "git"], // cwd: directory and session; git: branch and changes. Either can move to any line
|
|
95
97
|
"footerLine3Keys": [], // painted only when a chip is visible
|
|
96
98
|
}
|
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
33
33
|
| 配置面板 | `Style / Features / UI / Diff / Thinking / Footer` 六页签 | `/ccstyle` |
|
|
34
34
|
| 上下文检查 | 查看上下文占用,并预览 System prompt、Memory、Skills、Tools definition 和消息内容 | `/context` |
|
|
35
35
|
| Session/Subagent 引用 | 搜索并注入历史 Session 或现有 SubAgent 的有效上下文 | `@` |
|
|
36
|
-
| 状态栏 |
|
|
36
|
+
| 状态栏 | 显示:模型、上下文、缓存、费用、本次对话累计 token、git并适配 @narumitw/pi-usage 实时显示额度;git/缓存图标可关 Nerd Font | `/ccstyle` |
|
|
37
37
|
| 主题 | 随包提供内置 CC Dark、CC Light 主题 | `/settings` → Theme |
|
|
38
38
|
|
|
39
39
|
`on` / `compact` 两种模式的渲染样例:[默认模式](./docs/tool-render-examples-default.md) · [紧凑模式](./docs/tool-render-examples-compact.md)
|
|
@@ -77,7 +77,8 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
77
77
|
|
|
78
78
|
// thinking
|
|
79
79
|
"useSummaryTitlesAsThinkingTitle": true, // 用最新摘要作思考标题
|
|
80
|
-
"previewLines": 3, //
|
|
80
|
+
"previewLines": 3, // 思考中的预览行数,0 隐藏
|
|
81
|
+
"completedPreviewLines": null, // 完成态预览行数,0 隐藏;null 跟随 previewLines。面板可输入任意值或 follow
|
|
81
82
|
"animationIntervalMs": 90, // 标题动画间隔(毫秒)
|
|
82
83
|
|
|
83
84
|
// footer
|
|
@@ -85,12 +86,13 @@ pi install git:github.com/minuque/pi-cc-extensions
|
|
|
85
86
|
"footerTokenRate": true, // 输出速率芯片;false 隐藏
|
|
86
87
|
"footerTokenRateMode": "average", // average:整段指令平均(avg);live:最近 1 秒瞬时(live)
|
|
87
88
|
"footerTokenRateTtft": true, // 速率芯片是否带 TTFT;false 只显示 tok/s
|
|
89
|
+
"footerSessionTokens": false, // 本次对话累计 token 芯片(∑);累加会话条目的 usage.totalTokens,默认关
|
|
88
90
|
"footerCurrency": "USD", // 底栏费用的显示币种
|
|
89
91
|
"footerCurrencyRate": null, // 1 单位费用换成多少显示币种;null 只换符号、不乘倍率
|
|
90
92
|
"footerNerdIcons": true, // git/缓存用 Nerd Font 图标;false 为纯文本
|
|
91
93
|
"footerGitStatsMode": "working", // working(默认):未提交改动;branch:分支累计净改动
|
|
92
94
|
"footerHiddenKeys": [], // 隐藏的芯片 key
|
|
93
|
-
"footerLine1Keys": ["model", "context", "cache", "rate", "cost", "pi-usage"], // 每行芯片顺序;内置项与插件同一套排布
|
|
95
|
+
"footerLine1Keys": ["model", "context", "cache", "rate", "cost", "tokens", "pi-usage"], // 每行芯片顺序;内置项与插件同一套排布
|
|
94
96
|
"footerLine2Keys": ["cwd", "git"], // cwd:目录和会话名;git:分支和改动。可换到任意行
|
|
95
97
|
"footerLine3Keys": [] // 有可见芯片才占行
|
|
96
98
|
}
|
|
@@ -62,6 +62,8 @@ export type Config = {
|
|
|
62
62
|
toolLabelClip: number;
|
|
63
63
|
useSummaryTitlesAsThinkingTitle: boolean;
|
|
64
64
|
previewLines: number;
|
|
65
|
+
/** 思考完成后的折叠预览行数;null = 跟随 previewLines(保持旧行为)。 */
|
|
66
|
+
completedPreviewLines: number | null;
|
|
65
67
|
animationIntervalMs: number;
|
|
66
68
|
showStartupHeader: boolean;
|
|
67
69
|
/** fullscreen:userMessage 正文首行滚出视口上沿后钉住,直到下一条指令接手。 */
|
|
@@ -85,6 +87,8 @@ export type Config = {
|
|
|
85
87
|
footerTokenRateMode: FooterTokenRateMode;
|
|
86
88
|
/** 速率芯片是否带 TTFT。关则只显示速率。 */
|
|
87
89
|
footerTokenRateTtft: boolean;
|
|
90
|
+
/** 会话累计 token 芯片(累加会话条目的 usage.totalTokens)。默认关。 */
|
|
91
|
+
footerSessionTokens: boolean;
|
|
88
92
|
/** ISO 4217 显示币种。USD 时不换算。 */
|
|
89
93
|
footerCurrency: string;
|
|
90
94
|
/** 1 单位费用对应的显示币种数量。null 只换符号、不乘倍率。 */
|
|
@@ -119,6 +123,8 @@ export const EXPANDED_OUTPUT_MAX_LINES_VALUES = ["10", "20", "40", "80", "120"];
|
|
|
119
123
|
/** 工具摘要里 path/command 等输入的折叠字符数;0 = 只按可用宽度截断。 */
|
|
120
124
|
export const TOOL_LABEL_CLIP_VALUES = ["0", "40", "60", "80", "100", "120", "160"];
|
|
121
125
|
export const THINKING_PREVIEW_LINES_VALUES = ["0", "1", "3", "5", "10"];
|
|
126
|
+
/** 完成态思考预览预设;follow = 跟随 previewLines。 */
|
|
127
|
+
export const COMPLETED_THINKING_PREVIEW_LINES_VALUES = ["follow", ...THINKING_PREVIEW_LINES_VALUES];
|
|
122
128
|
export const THINKING_ANIMATION_INTERVAL_VALUES = ["40", "60", "90", "120", "180"];
|
|
123
129
|
/** Tools commonly toggled in excludeRenderers via the settings panel. */
|
|
124
130
|
export const EXCLUDE_RENDERER_CANDIDATES = [
|
|
@@ -150,6 +156,7 @@ export const DEFAULT_CONFIG: Config = {
|
|
|
150
156
|
toolLabelClip: 0,
|
|
151
157
|
useSummaryTitlesAsThinkingTitle: true,
|
|
152
158
|
previewLines: 3,
|
|
159
|
+
completedPreviewLines: null,
|
|
153
160
|
animationIntervalMs: 90,
|
|
154
161
|
showStartupHeader: true,
|
|
155
162
|
stickyUserMessage: true,
|
|
@@ -166,6 +173,7 @@ export const DEFAULT_CONFIG: Config = {
|
|
|
166
173
|
footerTokenRate: true,
|
|
167
174
|
footerTokenRateMode: "average",
|
|
168
175
|
footerTokenRateTtft: true,
|
|
176
|
+
footerSessionTokens: false,
|
|
169
177
|
footerCurrency: "USD",
|
|
170
178
|
footerCurrencyRate: null,
|
|
171
179
|
footerNerdIcons: true,
|
|
@@ -196,6 +204,13 @@ export function pickPositiveNumber(value: unknown, fallback: number, min = 1): n
|
|
|
196
204
|
return Number.isFinite(n) ? Math.max(min, n) : fallback;
|
|
197
205
|
}
|
|
198
206
|
|
|
207
|
+
/** 思考完成后的预览行数。null(缺失/空/非法)= 跟随 previewLines,保持旧配置行为。 */
|
|
208
|
+
export function pickCompletedPreviewLines(value: unknown): number | null {
|
|
209
|
+
if (value === undefined || value === null || value === "") return null;
|
|
210
|
+
const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
211
|
+
return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : null;
|
|
212
|
+
}
|
|
213
|
+
|
|
199
214
|
function normalizeCurrency(value: unknown): string {
|
|
200
215
|
return typeof value === "string" && /^[A-Za-z]{3}$/.test(value.trim())
|
|
201
216
|
? value.trim().toUpperCase()
|
|
@@ -277,6 +292,7 @@ export function normalizeConfig(input: unknown): Config {
|
|
|
277
292
|
0,
|
|
278
293
|
Number.MAX_SAFE_INTEGER,
|
|
279
294
|
),
|
|
295
|
+
completedPreviewLines: pickCompletedPreviewLines(source.completedPreviewLines),
|
|
280
296
|
animationIntervalMs: pickPositiveNumber(
|
|
281
297
|
source.animationIntervalMs,
|
|
282
298
|
DEFAULT_CONFIG.animationIntervalMs,
|
|
@@ -304,6 +320,7 @@ export function normalizeConfig(input: unknown): Config {
|
|
|
304
320
|
DEFAULT_CONFIG.footerTokenRateMode,
|
|
305
321
|
),
|
|
306
322
|
footerTokenRateTtft: source.footerTokenRateTtft !== false,
|
|
323
|
+
footerSessionTokens: source.footerSessionTokens === true,
|
|
307
324
|
footerCurrency: normalizeCurrency(source.footerCurrency),
|
|
308
325
|
footerCurrencyRate: normalizeFooterCurrencyRate(source.footerCurrencyRate),
|
|
309
326
|
footerNerdIcons: source.footerNerdIcons !== false,
|
|
@@ -320,6 +337,7 @@ export function getCompactThinkingConfig(source: Config = config): CompactThinki
|
|
|
320
337
|
return {
|
|
321
338
|
useSummaryTitlesAsThinkingTitle: source.useSummaryTitlesAsThinkingTitle,
|
|
322
339
|
previewLines: source.previewLines,
|
|
340
|
+
completedPreviewLines: source.completedPreviewLines,
|
|
323
341
|
animationIntervalMs: source.animationIntervalMs,
|
|
324
342
|
};
|
|
325
343
|
}
|
|
@@ -393,6 +411,7 @@ export function formatConfigStatus(source: Config = config): string {
|
|
|
393
411
|
`toolLabelClip=${source.toolLabelClip}`,
|
|
394
412
|
`thinkingTitle=${source.useSummaryTitlesAsThinkingTitle ? "summary" : "default"}`,
|
|
395
413
|
`thinkingPreview=${source.previewLines}`,
|
|
414
|
+
`thinkingPreviewDone=${source.completedPreviewLines ?? "follow"}`,
|
|
396
415
|
`thinkingAnimation=${source.animationIntervalMs}ms`,
|
|
397
416
|
`startupHeader=${source.showStartupHeader ? "on" : "off"}`,
|
|
398
417
|
`stickyUserMessage=${source.stickyUserMessage ? "on" : "off"}`,
|
|
@@ -409,6 +428,7 @@ export function formatConfigStatus(source: Config = config): string {
|
|
|
409
428
|
`footerTokenRate=${source.footerTokenRate ? "on" : "off"}`,
|
|
410
429
|
`footerTokenRateMode=${source.footerTokenRateMode}`,
|
|
411
430
|
`footerTokenRateTtft=${source.footerTokenRateTtft ? "on" : "off"}`,
|
|
431
|
+
`footerSessionTokens=${source.footerSessionTokens ? "on" : "off"}`,
|
|
412
432
|
`footerCurrency=${source.footerCurrency}`,
|
|
413
433
|
`footerRate=${source.footerCurrencyRate ?? "-"}`,
|
|
414
434
|
`footerIcons=${source.footerNerdIcons ? "nerd" : "plain"}`,
|
|
@@ -33,6 +33,7 @@ import { applyStartupHeader, clearStartupHeader } from "../feature/shell/startup
|
|
|
33
33
|
import type { ToolGroupingHooks } from "../renderer/tool/grouping.ts";
|
|
34
34
|
import {
|
|
35
35
|
config,
|
|
36
|
+
COMPLETED_THINKING_PREVIEW_LINES_VALUES,
|
|
36
37
|
DEFAULT_CONFIG,
|
|
37
38
|
DIFF_COLLAPSED_LINES_VALUES,
|
|
38
39
|
DIFF_INDICATOR_MODES,
|
|
@@ -45,6 +46,7 @@ import {
|
|
|
45
46
|
FOOTER_TOKEN_RATE_MODES,
|
|
46
47
|
formatExcludeRenderers,
|
|
47
48
|
getCompactThinkingConfig,
|
|
49
|
+
pickCompletedPreviewLines,
|
|
48
50
|
pickPositiveInt,
|
|
49
51
|
pickPositiveNumber,
|
|
50
52
|
pickToolLabelClip,
|
|
@@ -111,6 +113,12 @@ function footerTokenRateTtftDescription(enabled: boolean): string {
|
|
|
111
113
|
: "Time to first token hidden. The rate chip shows tok/s only.";
|
|
112
114
|
}
|
|
113
115
|
|
|
116
|
+
function footerSessionTokensDescription(enabled: boolean): string {
|
|
117
|
+
return enabled
|
|
118
|
+
? "Cumulative tokens used in this conversation, summed from the session's usage.totalTokens. Turn off to hide it."
|
|
119
|
+
: "Conversation token total hidden.";
|
|
120
|
+
}
|
|
121
|
+
|
|
114
122
|
function footerGitStatsDescription(mode: FooterGitStatsMode): string {
|
|
115
123
|
return mode === "branch"
|
|
116
124
|
? "Net changes since the merge base, including uncommitted edits. Base: origin/HEAD, then local main/master. Untracked files excluded; unavailable stats hidden."
|
|
@@ -363,20 +371,28 @@ function buildValueInputSubmenu(
|
|
|
363
371
|
};
|
|
364
372
|
}
|
|
365
373
|
|
|
366
|
-
/**
|
|
374
|
+
/** 数值项:预填当前值,空输入取消,非法输入留下错误;allowFollow 时额外接受 follow。 */
|
|
367
375
|
function buildNumberInputSubmenu(
|
|
368
376
|
theme: any,
|
|
369
377
|
setting: { label: string; currentValue: string },
|
|
370
378
|
closeSubmenu: (selected?: string) => void,
|
|
379
|
+
allowFollow = false,
|
|
371
380
|
) {
|
|
372
381
|
return buildValueInputSubmenu(
|
|
373
382
|
theme,
|
|
374
383
|
{
|
|
375
384
|
prompt: ` ${setting.label} — custom value:`,
|
|
376
385
|
initial: setting.currentValue,
|
|
377
|
-
hint:
|
|
378
|
-
|
|
379
|
-
|
|
386
|
+
hint: allowFollow
|
|
387
|
+
? " Enter to apply · Esc to go back · follow = Preview lines"
|
|
388
|
+
: " Enter to apply · Esc to go back",
|
|
389
|
+
accept: (raw) =>
|
|
390
|
+
raw === ""
|
|
391
|
+
? undefined
|
|
392
|
+
: (allowFollow && raw === "follow") || Number.isFinite(Number(raw))
|
|
393
|
+
? raw
|
|
394
|
+
: null,
|
|
395
|
+
invalid: (raw) => `${allowFollow ? "Invalid value" : "Invalid number"}: "${raw}"`,
|
|
380
396
|
},
|
|
381
397
|
closeSubmenu,
|
|
382
398
|
);
|
|
@@ -575,12 +591,23 @@ export async function showCcstylePanel(
|
|
|
575
591
|
const thinkingPreviewSetting = {
|
|
576
592
|
id: "previewLines",
|
|
577
593
|
label: "Preview lines",
|
|
578
|
-
description: "Thinking preview lines; 0 hides the preview body.",
|
|
594
|
+
description: "Thinking preview lines while a run is in progress; 0 hides the preview body.",
|
|
579
595
|
currentValue: String(config.previewLines),
|
|
580
596
|
values: [...THINKING_PREVIEW_LINES_VALUES],
|
|
581
597
|
submenu: (_current: string, closeSubmenu: (selected?: string) => void) =>
|
|
582
598
|
buildNumberInputSubmenu(theme, thinkingPreviewSetting, closeSubmenu),
|
|
583
599
|
};
|
|
600
|
+
const completedThinkingPreviewSetting = {
|
|
601
|
+
id: "completedPreviewLines",
|
|
602
|
+
label: "Completed preview",
|
|
603
|
+
description:
|
|
604
|
+
"Thinking preview lines once the run finishes; follow = same as Preview lines. Enter to type a custom value or follow.",
|
|
605
|
+
currentValue:
|
|
606
|
+
config.completedPreviewLines === null ? "follow" : String(config.completedPreviewLines),
|
|
607
|
+
values: [...COMPLETED_THINKING_PREVIEW_LINES_VALUES],
|
|
608
|
+
submenu: (_current: string, closeSubmenu: (selected?: string) => void) =>
|
|
609
|
+
buildNumberInputSubmenu(theme, completedThinkingPreviewSetting, closeSubmenu, true),
|
|
610
|
+
};
|
|
584
611
|
const thinkingAnimationSetting = {
|
|
585
612
|
id: "animationIntervalMs",
|
|
586
613
|
label: "Animation interval ms",
|
|
@@ -781,6 +808,13 @@ export async function showCcstylePanel(
|
|
|
781
808
|
currentValue: config.footerTokenRateTtft ? "on" : "off",
|
|
782
809
|
values: ["on", "off"],
|
|
783
810
|
};
|
|
811
|
+
const footerSessionTokensSetting = {
|
|
812
|
+
id: "footerSessionTokens",
|
|
813
|
+
label: "Conversation tokens",
|
|
814
|
+
description: footerSessionTokensDescription(config.footerSessionTokens),
|
|
815
|
+
currentValue: config.footerSessionTokens ? "on" : "off",
|
|
816
|
+
values: ["on", "off"],
|
|
817
|
+
};
|
|
784
818
|
const pluginChipsSetting = {
|
|
785
819
|
id: "footerPluginChips",
|
|
786
820
|
label: "Footer chips",
|
|
@@ -879,6 +913,14 @@ export async function showCcstylePanel(
|
|
|
879
913
|
ctx.ui.notify(`Updated ${id}: ${value}`, "info");
|
|
880
914
|
return;
|
|
881
915
|
}
|
|
916
|
+
if (id === "footerSessionTokens") {
|
|
917
|
+
const enabled = value === "on";
|
|
918
|
+
updateConfig({ footerSessionTokens: enabled });
|
|
919
|
+
footerSessionTokensSetting.currentValue = enabled ? "on" : "off";
|
|
920
|
+
footerSessionTokensSetting.description = footerSessionTokensDescription(enabled);
|
|
921
|
+
ctx.ui.notify(`Updated ${id}: ${value}`, "info");
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
882
924
|
if (id === "enableEditorPrefix") {
|
|
883
925
|
const enabled = value === "on";
|
|
884
926
|
updateConfig({ enableEditorPrefix: enabled });
|
|
@@ -1007,6 +1049,13 @@ export async function showCcstylePanel(
|
|
|
1007
1049
|
updateConfig({ previewLines: pickPositiveInt(value, DEFAULT_CONFIG.previewLines, 0) });
|
|
1008
1050
|
thinkingPreviewSetting.currentValue = String(config.previewLines);
|
|
1009
1051
|
break;
|
|
1052
|
+
case "completedPreviewLines":
|
|
1053
|
+
updateConfig({
|
|
1054
|
+
completedPreviewLines: value === "follow" ? null : pickCompletedPreviewLines(value),
|
|
1055
|
+
});
|
|
1056
|
+
completedThinkingPreviewSetting.currentValue =
|
|
1057
|
+
config.completedPreviewLines === null ? "follow" : String(config.completedPreviewLines);
|
|
1058
|
+
break;
|
|
1010
1059
|
case "animationIntervalMs":
|
|
1011
1060
|
updateConfig({
|
|
1012
1061
|
animationIntervalMs: pickPositiveNumber(value, DEFAULT_CONFIG.animationIntervalMs),
|
|
@@ -1094,7 +1143,12 @@ export async function showCcstylePanel(
|
|
|
1094
1143
|
{
|
|
1095
1144
|
id: "thinking",
|
|
1096
1145
|
label: "Thinking",
|
|
1097
|
-
items: [
|
|
1146
|
+
items: [
|
|
1147
|
+
thinkingTitleSetting,
|
|
1148
|
+
thinkingPreviewSetting,
|
|
1149
|
+
completedThinkingPreviewSetting,
|
|
1150
|
+
thinkingAnimationSetting,
|
|
1151
|
+
],
|
|
1098
1152
|
},
|
|
1099
1153
|
{
|
|
1100
1154
|
id: "footer",
|
|
@@ -1104,6 +1158,7 @@ export async function showCcstylePanel(
|
|
|
1104
1158
|
footerTokenRateSetting,
|
|
1105
1159
|
footerTokenRateModeSetting,
|
|
1106
1160
|
footerTokenRateTtftSetting,
|
|
1161
|
+
footerSessionTokensSetting,
|
|
1107
1162
|
footerCurrencySetting,
|
|
1108
1163
|
footerCurrencyRateSetting,
|
|
1109
1164
|
footerNerdIconsSetting,
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* 计数:bash/powershell 按调用次数;read/edit/write 按非空 path/file_path 去重;other 按调用次数。
|
|
6
6
|
* 失败单独累计;另记回合耗时。
|
|
7
7
|
*
|
|
8
|
-
* 呈现:`
|
|
8
|
+
* 呈现:`summarySegments` 分段(文字 + 语义色),渲染层自己拼行并上色。
|
|
9
|
+
* 不要经 Markdown 渲染:pi 的 Markdown 组件会剥掉输入里的终端序列,数字会失去语义色。
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import { formatDuration } from "../../utils/format.ts";
|
|
13
13
|
|
|
14
14
|
/** 工具分类。 */
|
|
15
15
|
export type AgentToolCategory = "bash" | "read" | "edit" | "write" | "other";
|
|
@@ -113,25 +113,20 @@ function summaryParts(data: AgentSummaryData): string[] {
|
|
|
113
113
|
return parts;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
):
|
|
116
|
+
/** 摘要段:一段文字 + 语义色;数字上色由渲染层完成。 */
|
|
117
|
+
export type SummarySegment = { text: string; tone: "success" | "failed" };
|
|
118
|
+
|
|
119
|
+
export function summarySegments(data: AgentSummaryData): SummarySegment[] {
|
|
120
120
|
const parts = summaryParts(data);
|
|
121
|
-
if (parts.length === 0) return
|
|
121
|
+
if (parts.length === 0) return [];
|
|
122
122
|
const capitalizeFirst = (part: string, first: boolean) => {
|
|
123
123
|
const verb = part.match(/^[a-z]+/)?.[0] ?? "";
|
|
124
124
|
return first && verb ? verb[0].toUpperCase() + verb.slice(1) + part.slice(verb.length) : part;
|
|
125
125
|
};
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
.map((part) => paintNumber(part.endsWith("failed") ? colors.failed : colors.success, part))
|
|
131
|
-
.join(", ");
|
|
132
|
-
const duration = formatDuration(data.durationMs);
|
|
133
|
-
const line = duration ? `${text} · ${duration}` : text;
|
|
134
|
-
return `> *${line}*`;
|
|
126
|
+
return parts.map((part, index) => ({
|
|
127
|
+
text: capitalizeFirst(part, index === 0),
|
|
128
|
+
tone: part.endsWith("failed") ? "failed" : "success",
|
|
129
|
+
}));
|
|
135
130
|
}
|
|
136
131
|
|
|
137
132
|
/**
|
|
@@ -1,27 +1,76 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Agent 回合摘要展示:agent_end 时把本回合工具统计写入会话条目,
|
|
3
|
-
* 由 entry renderer
|
|
3
|
+
* 由 entry renderer 渲染成一行引用行 `│ Ran 1 command, wrote 1 file · 15s`。
|
|
4
4
|
*
|
|
5
5
|
* 统计复用 ./core.ts(bash|powershell/read/edit/write/other)。
|
|
6
6
|
* appendEntry 不进 LLM 上下文,只显示在聊天区。
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
|
+
* 不走 Markdown:pi 的 Markdown 组件会剥掉输入里的终端序列,
|
|
9
|
+
* 数字的语义色只能由组件自己拼。竖线取 mdQuoteBorder,文字取 mdQuote + 斜体,数字取 success/error。
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
|
-
import {
|
|
11
|
-
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
12
13
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import {
|
|
14
|
+
import { formatDuration } from "../../utils/format.ts";
|
|
15
|
+
import { bindAgentSummary, summarySegments, type SummarySegment } from "./core.ts";
|
|
16
|
+
import type { AgentSummaryData } from "./core.ts";
|
|
14
17
|
|
|
15
18
|
export const AGENT_SUMMARY_ENTRY_TYPE = "agent-summary";
|
|
16
19
|
|
|
20
|
+
type SummaryTheme = {
|
|
21
|
+
fg(color: string, text: string): string;
|
|
22
|
+
italic(text: string): string;
|
|
23
|
+
getFgAnsi(color: string): string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** 取样式函数在正文前产生的 SGR 前缀;行内 reset 之后要靠它把样式续上。 */
|
|
27
|
+
function stylePrefix(style: (text: string) => string): string {
|
|
28
|
+
const sentinel = "\u0000";
|
|
29
|
+
const styled = style(sentinel);
|
|
30
|
+
const index = styled.indexOf(sentinel);
|
|
31
|
+
return index >= 0 ? styled.slice(0, index) : "";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 只给段内第一个数字上色;没有数字的段原样返回。 */
|
|
35
|
+
function paintNumber(segment: SummarySegment, theme: SummaryTheme): string {
|
|
36
|
+
const match = /(\d+)/.exec(segment.text);
|
|
37
|
+
if (!match) return segment.text;
|
|
38
|
+
const ansi = theme.getFgAnsi(segment.tone === "failed" ? "error" : "success");
|
|
39
|
+
if (!ansi) return segment.text;
|
|
40
|
+
const start = match.index;
|
|
41
|
+
return `${segment.text.slice(0, start)}${ansi}${match[1]}\x1b[0m${segment.text.slice(start + match[1].length)}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 摘要行:左侧 mdQuoteBorder 竖线 + 斜体 mdQuote 正文,数字按成功/失败上色。 */
|
|
45
|
+
class AgentSummaryLine {
|
|
46
|
+
private readonly body: string;
|
|
47
|
+
private readonly border: string;
|
|
48
|
+
private readonly theme: SummaryTheme;
|
|
49
|
+
|
|
50
|
+
constructor(segments: readonly SummarySegment[], duration: string, theme: SummaryTheme) {
|
|
51
|
+
this.theme = theme;
|
|
52
|
+
this.border = theme.fg("mdQuoteBorder", "│");
|
|
53
|
+
const quoteStyle = (text: string) => theme.italic(theme.fg("mdQuote", text));
|
|
54
|
+
const counts = segments.map((segment) => paintNumber(segment, theme)).join(", ");
|
|
55
|
+
const text = duration ? `${counts} · ${duration}` : counts;
|
|
56
|
+
const prefix = stylePrefix(quoteStyle);
|
|
57
|
+
this.body = quoteStyle(prefix ? text.replace(/\x1b\[0m/g, `\x1b[0m${prefix}`) : text);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
invalidate(): void {}
|
|
61
|
+
|
|
62
|
+
render(width: number): string[] {
|
|
63
|
+
const contentWidth = Math.max(1, width - 4);
|
|
64
|
+
return wrapTextWithAnsi(this.body, contentWidth).map((line) => ` ${this.border} ${line}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
17
68
|
export default function (pi: ExtensionAPI): void {
|
|
18
69
|
pi.registerEntryRenderer(AGENT_SUMMARY_ENTRY_TYPE, (entry, _options, theme) => {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (!line) return undefined;
|
|
24
|
-
return new Markdown(line, 1, 0, getMarkdownTheme());
|
|
70
|
+
const segments = summarySegments(entry.data as AgentSummaryData);
|
|
71
|
+
if (segments.length === 0) return undefined;
|
|
72
|
+
const duration = formatDuration((entry.data as AgentSummaryData).durationMs);
|
|
73
|
+
return new AgentSummaryLine(segments, duration, theme as unknown as SummaryTheme);
|
|
25
74
|
});
|
|
26
75
|
|
|
27
76
|
bindAgentSummary(pi, (data) => {
|
|
@@ -41,6 +41,8 @@ type AssistantMessageComponentLike = InstanceType<typeof AssistantMessageCompone
|
|
|
41
41
|
export type CompactThinkingConfig = {
|
|
42
42
|
useSummaryTitlesAsThinkingTitle: boolean;
|
|
43
43
|
previewLines: number;
|
|
44
|
+
/** 思考完成后的折叠预览行数;null = 跟随 previewLines(保持旧行为)。 */
|
|
45
|
+
completedPreviewLines: number | null;
|
|
44
46
|
animationIntervalMs: number;
|
|
45
47
|
};
|
|
46
48
|
|
|
@@ -112,6 +114,7 @@ type PatchedPrototype = typeof AssistantMessageComponent.prototype & {
|
|
|
112
114
|
const config: CompactThinkingConfig = {
|
|
113
115
|
useSummaryTitlesAsThinkingTitle: true,
|
|
114
116
|
previewLines: 3,
|
|
117
|
+
completedPreviewLines: null,
|
|
115
118
|
animationIntervalMs: 90,
|
|
116
119
|
};
|
|
117
120
|
|
|
@@ -273,11 +276,12 @@ function layoutThinkingPreview(
|
|
|
273
276
|
text: string,
|
|
274
277
|
width: number,
|
|
275
278
|
padding: number,
|
|
279
|
+
previewLines: number,
|
|
276
280
|
): { visible: string[]; hiddenLines: number } {
|
|
277
|
-
if (!text ||
|
|
281
|
+
if (!text || previewLines <= 0) return { visible: [], hiddenLines: 0 };
|
|
278
282
|
const contentWidth = Math.max(1, width - padding * 2);
|
|
279
283
|
const budget = Math.max(
|
|
280
|
-
width * (
|
|
284
|
+
width * (previewLines + PREVIEW_BUDGET_SLACK_LINES),
|
|
281
285
|
PREVIEW_BUDGET_MIN_CHARS,
|
|
282
286
|
);
|
|
283
287
|
let source = text;
|
|
@@ -298,8 +302,8 @@ function layoutThinkingPreview(
|
|
|
298
302
|
};
|
|
299
303
|
cachePreview(cacheKey, entry);
|
|
300
304
|
}
|
|
301
|
-
const hiddenLines = entry.hiddenBefore + Math.max(0, entry.lines.length -
|
|
302
|
-
const visible = hiddenLines > 0 ? entry.lines.slice(-
|
|
305
|
+
const hiddenLines = entry.hiddenBefore + Math.max(0, entry.lines.length - previewLines);
|
|
306
|
+
const visible = hiddenLines > 0 ? entry.lines.slice(-previewLines) : entry.lines;
|
|
303
307
|
return { visible, hiddenLines };
|
|
304
308
|
}
|
|
305
309
|
|
|
@@ -335,6 +339,8 @@ export class ThinkingPreviewBlock implements Component {
|
|
|
335
339
|
private style: (text: string) => string;
|
|
336
340
|
private theme: Theme | undefined;
|
|
337
341
|
private hintHovered = false;
|
|
342
|
+
/** 构造时的活动态:活动块用 previewLines,完成块用 completedPreviewLines。 */
|
|
343
|
+
private active: boolean;
|
|
338
344
|
private collapsedMemo:
|
|
339
345
|
| {
|
|
340
346
|
width: number;
|
|
@@ -351,8 +357,9 @@ export class ThinkingPreviewBlock implements Component {
|
|
|
351
357
|
padding: number,
|
|
352
358
|
messageTimestamp: number,
|
|
353
359
|
style: (text: string) => string,
|
|
354
|
-
theme
|
|
355
|
-
runStartIndex
|
|
360
|
+
theme: Theme | undefined,
|
|
361
|
+
runStartIndex: number,
|
|
362
|
+
active: boolean,
|
|
356
363
|
) {
|
|
357
364
|
this.heading = heading;
|
|
358
365
|
this.text = text;
|
|
@@ -361,6 +368,14 @@ export class ThinkingPreviewBlock implements Component {
|
|
|
361
368
|
this.runStartIndex = runStartIndex;
|
|
362
369
|
this.style = style;
|
|
363
370
|
this.theme = theme;
|
|
371
|
+
this.active = active;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** 活动块用 previewLines;完成块用 completedPreviewLines(null 跟随 previewLines)。 */
|
|
375
|
+
private effectivePreviewLines(): number {
|
|
376
|
+
return this.active
|
|
377
|
+
? config.previewLines
|
|
378
|
+
: (config.completedPreviewLines ?? config.previewLines);
|
|
364
379
|
}
|
|
365
380
|
|
|
366
381
|
private paint(color: string, text: string): string {
|
|
@@ -393,7 +408,7 @@ export class ThinkingPreviewBlock implements Component {
|
|
|
393
408
|
private headingLines(width: number, hiddenLines: number, padding: number): string[] {
|
|
394
409
|
const hint = this.expanded
|
|
395
410
|
? undefined
|
|
396
|
-
: hiddenPreviewHint(hiddenLines, Boolean(this.text) &&
|
|
411
|
+
: hiddenPreviewHint(hiddenLines, Boolean(this.text) && this.effectivePreviewLines() <= 0);
|
|
397
412
|
if (!hint) return new Text(this.heading, padding, 0).render(width);
|
|
398
413
|
const rawHint = hint.prefix + hint.action + hint.suffix;
|
|
399
414
|
const contentWidth = Math.max(1, width - padding * 2);
|
|
@@ -411,7 +426,7 @@ export class ThinkingPreviewBlock implements Component {
|
|
|
411
426
|
}
|
|
412
427
|
|
|
413
428
|
private bodyLines(width: number, padding: number): { lines: string[]; hiddenLines: number } {
|
|
414
|
-
if (!this.text || (
|
|
429
|
+
if (!this.text || (this.effectivePreviewLines() <= 0 && !this.expanded)) {
|
|
415
430
|
return { lines: [], hiddenLines: 0 };
|
|
416
431
|
}
|
|
417
432
|
if (this.expanded) {
|
|
@@ -420,7 +435,7 @@ export class ThinkingPreviewBlock implements Component {
|
|
|
420
435
|
hiddenLines: 0,
|
|
421
436
|
};
|
|
422
437
|
}
|
|
423
|
-
const preview = layoutThinkingPreview(this.text, width, padding);
|
|
438
|
+
const preview = layoutThinkingPreview(this.text, width, padding, this.effectivePreviewLines());
|
|
424
439
|
return { lines: preview.visible, hiddenLines: preview.hiddenLines };
|
|
425
440
|
}
|
|
426
441
|
|
|
@@ -447,7 +462,7 @@ export class ThinkingPreviewBlock implements Component {
|
|
|
447
462
|
const expandAction = thinkingExpandAction();
|
|
448
463
|
if (
|
|
449
464
|
this.collapsedMemo?.width === width &&
|
|
450
|
-
this.collapsedMemo.previewLines ===
|
|
465
|
+
this.collapsedMemo.previewLines === this.effectivePreviewLines() &&
|
|
451
466
|
this.collapsedMemo.hintHovered === this.hintHovered &&
|
|
452
467
|
this.collapsedMemo.expandAction === expandAction
|
|
453
468
|
) {
|
|
@@ -464,7 +479,7 @@ export class ThinkingPreviewBlock implements Component {
|
|
|
464
479
|
: heading;
|
|
465
480
|
this.collapsedMemo = {
|
|
466
481
|
width,
|
|
467
|
-
previewLines:
|
|
482
|
+
previewLines: this.effectivePreviewLines(),
|
|
468
483
|
hintHovered: this.hintHovered,
|
|
469
484
|
expandAction,
|
|
470
485
|
lines,
|
|
@@ -715,6 +730,8 @@ function compactThinking(pi: ExtensionAPI) {
|
|
|
715
730
|
}
|
|
716
731
|
const runEndIndex = i - 1;
|
|
717
732
|
i--;
|
|
733
|
+
// Keep this run-scoped snapshot: a timestamp-only render query would mark
|
|
734
|
+
// earlier thinking runs active when one message alternates text and thinking.
|
|
718
735
|
const active = isActiveRun(message, runStartIndex, runEndIndex);
|
|
719
736
|
// OpenAI can spend several seconds reasoning before it emits the first
|
|
720
737
|
// summary token. Keep an empty active block visible as animated
|
|
@@ -759,6 +776,7 @@ function compactThinking(pi: ExtensionAPI) {
|
|
|
759
776
|
thinkingStyle,
|
|
760
777
|
activeTheme,
|
|
761
778
|
runStartIndex,
|
|
779
|
+
active,
|
|
762
780
|
),
|
|
763
781
|
);
|
|
764
782
|
|
|
@@ -13,6 +13,8 @@ export const FOOTER_CONTEXT_KEY = "context";
|
|
|
13
13
|
export const FOOTER_CACHE_KEY = "cache";
|
|
14
14
|
export const FOOTER_RATE_KEY = "rate";
|
|
15
15
|
export const FOOTER_COST_KEY = "cost";
|
|
16
|
+
/** 本次对话累计消耗 token(累加会话条目的 usage.totalTokens)。 */
|
|
17
|
+
export const FOOTER_TOKENS_KEY = "tokens";
|
|
16
18
|
export const FOOTER_CWD_KEY = "cwd";
|
|
17
19
|
export const FOOTER_GIT_KEY = "git";
|
|
18
20
|
|
|
@@ -23,6 +25,7 @@ export const DEFAULT_FOOTER_LINE1_KEYS = [
|
|
|
23
25
|
FOOTER_CACHE_KEY,
|
|
24
26
|
FOOTER_RATE_KEY,
|
|
25
27
|
FOOTER_COST_KEY,
|
|
28
|
+
FOOTER_TOKENS_KEY,
|
|
26
29
|
PI_USAGE_KEY,
|
|
27
30
|
] as const;
|
|
28
31
|
|
|
@@ -40,7 +43,18 @@ const LEGACY_FIXED_FOOTER_KEYS = [
|
|
|
40
43
|
FOOTER_GIT_KEY,
|
|
41
44
|
] as const;
|
|
42
45
|
|
|
43
|
-
|
|
46
|
+
/**
|
|
47
|
+
* 后加的内置芯片:老配置里不会出现,读取时补到默认行上 `after` 那个芯片后面。
|
|
48
|
+
* 与 LEGACY_FIXED_FOOTER_KEYS 分开,旧配置的「只排插件芯片」迁移判断才不会误判。
|
|
49
|
+
*/
|
|
50
|
+
const ADDED_FOOTER_KEYS: ReadonlyArray<{ key: string; after: string }> = [
|
|
51
|
+
{ key: FOOTER_TOKENS_KEY, after: FOOTER_COST_KEY },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const BUILTIN_FOOTER_KEYS = new Set<string>([
|
|
55
|
+
...LEGACY_FIXED_FOOTER_KEYS,
|
|
56
|
+
...ADDED_FOOTER_KEYS.map((item) => item.key),
|
|
57
|
+
]);
|
|
44
58
|
|
|
45
59
|
export function isBuiltinFooterKey(key: string): boolean {
|
|
46
60
|
return BUILTIN_FOOTER_KEYS.has(key);
|
|
@@ -62,9 +76,12 @@ export const DEFAULT_FOOTER_CHIP_LAYOUT: FooterChipLayout = {
|
|
|
62
76
|
footerLine3Keys: [],
|
|
63
77
|
};
|
|
64
78
|
|
|
65
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* 别的扩展写的 model / tokens 状态与本扩展的内置芯片重名,不当插件芯片接纳。
|
|
81
|
+
* 配置里的同名内置 key 不受影响。
|
|
82
|
+
*/
|
|
66
83
|
export function isSkippedFooterStatusKey(key: string): boolean {
|
|
67
|
-
return key === FOOTER_MODEL_KEY;
|
|
84
|
+
return key === FOOTER_MODEL_KEY || key === FOOTER_TOKENS_KEY;
|
|
68
85
|
}
|
|
69
86
|
|
|
70
87
|
/** 未单独配置时的默认行:内置项按原来的行,usage 类在 line1,其余 line2。不会默认进 line3。 */
|
|
@@ -113,7 +130,10 @@ function ensureBuiltinKeys(
|
|
|
113
130
|
if (LEGACY_FIXED_FOOTER_KEYS.some((key) => seen.has(key))) {
|
|
114
131
|
return { footerLine1Keys: line1, footerLine2Keys: line2, footerLine3Keys: line3 };
|
|
115
132
|
}
|
|
116
|
-
|
|
133
|
+
// tokens 属于后加芯片,由 ensureAddedBuiltins 负责插入。
|
|
134
|
+
const line1Builtins = DEFAULT_FOOTER_LINE1_KEYS.filter(
|
|
135
|
+
(key) => key !== PI_USAGE_KEY && key !== FOOTER_TOKENS_KEY,
|
|
136
|
+
);
|
|
117
137
|
const usageAt = line1.indexOf(PI_USAGE_KEY);
|
|
118
138
|
const footerLine1Keys =
|
|
119
139
|
usageAt >= 0
|
|
@@ -126,6 +146,33 @@ function ensureBuiltinKeys(
|
|
|
126
146
|
};
|
|
127
147
|
}
|
|
128
148
|
|
|
149
|
+
function ensureAddedBuiltins(layout: FooterChipLayout): FooterChipLayout {
|
|
150
|
+
const present = new Set([
|
|
151
|
+
...layout.footerLine1Keys,
|
|
152
|
+
...layout.footerLine2Keys,
|
|
153
|
+
...layout.footerLine3Keys,
|
|
154
|
+
...layout.footerHiddenKeys,
|
|
155
|
+
]);
|
|
156
|
+
const missing = ADDED_FOOTER_KEYS.filter((item) => !present.has(item.key));
|
|
157
|
+
if (missing.length === 0) return layout;
|
|
158
|
+
const next: FooterChipLayout = {
|
|
159
|
+
...layout,
|
|
160
|
+
footerLine1Keys: [...layout.footerLine1Keys],
|
|
161
|
+
footerLine2Keys: [...layout.footerLine2Keys],
|
|
162
|
+
footerLine3Keys: [...layout.footerLine3Keys],
|
|
163
|
+
};
|
|
164
|
+
for (const item of missing) {
|
|
165
|
+
// 贴着锚点芯片所在的行放(如 tokens 跟 cost)。
|
|
166
|
+
const line = footerLineOfKey(next, item.after) ?? defaultFooterLine(item.key);
|
|
167
|
+
const keys = keysOnLine(next, line);
|
|
168
|
+
const anchorAt = keys.indexOf(item.after);
|
|
169
|
+
// 锚点被用户删掉时接到该行末尾:芯片留在布局里,面板菜单与开关才管得到。
|
|
170
|
+
// 默认关闭,不渲染就不占位,多出来的 key 不会让底栏多一行。
|
|
171
|
+
keys.splice(anchorAt >= 0 ? anchorAt + 1 : keys.length, 0, item.key);
|
|
172
|
+
}
|
|
173
|
+
return next;
|
|
174
|
+
}
|
|
175
|
+
|
|
129
176
|
/** 跨行去重(line1 → line2 → line3);旧配置补上内置芯片。 */
|
|
130
177
|
export function normalizeFooterChipLayout(source: Record<string, unknown>): FooterChipLayout {
|
|
131
178
|
const hasHidden = Array.isArray(source.footerHiddenKeys);
|
|
@@ -144,10 +191,10 @@ export function normalizeFooterChipLayout(source: Record<string, unknown>): Foot
|
|
|
144
191
|
const line1 = takeUnique(normalizeFooterKeyList(source.footerLine1Keys), seen);
|
|
145
192
|
const line2 = takeUnique(normalizeFooterKeyList(source.footerLine2Keys), seen);
|
|
146
193
|
const line3 = takeUnique(normalizeFooterKeyList(source.footerLine3Keys), seen);
|
|
147
|
-
return {
|
|
194
|
+
return ensureAddedBuiltins({
|
|
148
195
|
footerHiddenKeys: normalizeFooterKeyList(source.footerHiddenKeys),
|
|
149
196
|
...ensureBuiltinKeys(line1, line2, line3),
|
|
150
|
-
};
|
|
197
|
+
});
|
|
151
198
|
}
|
|
152
199
|
|
|
153
200
|
const BUILTIN_FOOTER_LABEL: Record<string, string> = {
|
|
@@ -156,6 +203,7 @@ const BUILTIN_FOOTER_LABEL: Record<string, string> = {
|
|
|
156
203
|
[FOOTER_CACHE_KEY]: "Cache hit rate",
|
|
157
204
|
[FOOTER_RATE_KEY]: "Output rate",
|
|
158
205
|
[FOOTER_COST_KEY]: "Session cost",
|
|
206
|
+
[FOOTER_TOKENS_KEY]: "Tokens used in this conversation",
|
|
159
207
|
[FOOTER_CWD_KEY]: "Working directory and session name",
|
|
160
208
|
[FOOTER_GIT_KEY]: "Branch and change stats",
|
|
161
209
|
};
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 默认第一行:model thinking · ██████░░░░ 45%/200k · 42% · $0.01 · pi-usage · 其余 line1 芯片
|
|
5
5
|
* 默认第二行:cwd in session · branch (+16 −1) · 其余 line2 芯片
|
|
6
6
|
* 第三行:仅当 line3 有可见芯片时出现
|
|
7
|
-
* model / context / cache / rate / cost / cwd / git 与插件芯片同一套排布,可换行、调序、隐藏
|
|
7
|
+
* model / context / cache / rate / cost / tokens / cwd / git 与插件芯片同一套排布,可换行、调序、隐藏
|
|
8
8
|
*
|
|
9
9
|
* - line1 短芯片,· 分隔;缓存用 zentui ,费用 success
|
|
10
10
|
* - line2 用 zentui 句式 in / on +
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
FOOTER_GIT_KEY,
|
|
32
32
|
FOOTER_MODEL_KEY,
|
|
33
33
|
FOOTER_RATE_KEY,
|
|
34
|
+
FOOTER_TOKENS_KEY,
|
|
34
35
|
PI_USAGE_KEY,
|
|
35
36
|
isBuiltinFooterKey,
|
|
36
37
|
isSkippedFooterStatusKey,
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
type FooterChipLayout,
|
|
40
41
|
} from "./footer-layout.ts";
|
|
41
42
|
import { createGitStatsRefresher, readGitStats, type GitStats } from "./git-stats.ts";
|
|
43
|
+
import { formatSessionTokenTotal, sessionEntryTokens } from "./session-tokens.ts";
|
|
42
44
|
import { MCP_STATUS_KEY, buildMcpChip } from "./mcp-chip.ts";
|
|
43
45
|
import { inputTextIndent } from "./prompt-editor.ts";
|
|
44
46
|
import { currentTtftMs, displayTokenRate, formatTokenRate } from "./token-rate.ts";
|
|
@@ -391,7 +393,12 @@ const createCustomFooterFactory =
|
|
|
391
393
|
let statsMemoKey = "";
|
|
392
394
|
let statsMemoAt = 0;
|
|
393
395
|
let statsMemo:
|
|
394
|
-
| {
|
|
396
|
+
| {
|
|
397
|
+
contextUsage: ReturnType<typeof ctx.getContextUsage>;
|
|
398
|
+
cost: number;
|
|
399
|
+
tokens: number;
|
|
400
|
+
cachePct: number;
|
|
401
|
+
}
|
|
395
402
|
| undefined;
|
|
396
403
|
const sessionStats = () => {
|
|
397
404
|
const model = ctx.model;
|
|
@@ -404,19 +411,28 @@ const createCustomFooterFactory =
|
|
|
404
411
|
return statsMemo;
|
|
405
412
|
}
|
|
406
413
|
let cost = 0;
|
|
414
|
+
let tokens = 0;
|
|
407
415
|
for (const entry of ctx.sessionManager.getEntries()) {
|
|
408
416
|
if (entry.type === "message") {
|
|
409
417
|
const m = entry.message;
|
|
410
418
|
if (m.role === "assistant") cost += m.usage?.cost?.total || 0;
|
|
411
419
|
else if (m.role === "toolResult" && m.usage) cost += m.usage.cost?.total || 0;
|
|
412
420
|
} else if (
|
|
413
|
-
(entry.type === "branch_summary" ||
|
|
421
|
+
(entry.type === "branch_summary" ||
|
|
422
|
+
entry.type === "compaction" ||
|
|
423
|
+
entry.type === "usage") &&
|
|
414
424
|
entry.usage
|
|
415
425
|
) {
|
|
416
426
|
cost += entry.usage.cost?.total || 0;
|
|
417
427
|
}
|
|
428
|
+
tokens += sessionEntryTokens(entry);
|
|
418
429
|
}
|
|
419
|
-
statsMemo = {
|
|
430
|
+
statsMemo = {
|
|
431
|
+
contextUsage: ctx.getContextUsage(),
|
|
432
|
+
cost,
|
|
433
|
+
tokens,
|
|
434
|
+
cachePct: getCachePct(),
|
|
435
|
+
};
|
|
420
436
|
statsMemoKey = key;
|
|
421
437
|
statsMemoAt = performance.now();
|
|
422
438
|
return statsMemo;
|
|
@@ -424,7 +440,7 @@ const createCustomFooterFactory =
|
|
|
424
440
|
|
|
425
441
|
const render = (width: number): string[] => {
|
|
426
442
|
const model = ctx.model;
|
|
427
|
-
const { contextUsage, cost, cachePct } = sessionStats();
|
|
443
|
+
const { contextUsage, cost, tokens, cachePct } = sessionStats();
|
|
428
444
|
const contextWindow = contextUsage?.contextWindow ?? model?.contextWindow ?? 0;
|
|
429
445
|
const percent = contextUsage?.percent;
|
|
430
446
|
|
|
@@ -471,6 +487,7 @@ const createCustomFooterFactory =
|
|
|
471
487
|
const costChip = costLabel
|
|
472
488
|
? theme.fg("dim", costLabel) + (usingSubscription ? theme.fg("warning", " sub") : "")
|
|
473
489
|
: "";
|
|
490
|
+
const tokensLabel = config.footerSessionTokens ? formatSessionTokenTotal(tokens) : "";
|
|
474
491
|
|
|
475
492
|
const gitColor = theme.getThinkingBorderColor("medium");
|
|
476
493
|
let place = theme.fg("accent", cwdLabel);
|
|
@@ -508,6 +525,7 @@ const createCustomFooterFactory =
|
|
|
508
525
|
[FOOTER_CACHE_KEY, cacheLabel ? theme.fg("dim", cacheLabel) : ""],
|
|
509
526
|
[FOOTER_RATE_KEY, rateLabel ? theme.fg("dim", rateLabel) : ""],
|
|
510
527
|
[FOOTER_COST_KEY, costChip],
|
|
528
|
+
[FOOTER_TOKENS_KEY, tokensLabel ? theme.fg("dim", tokensLabel) : ""],
|
|
511
529
|
[FOOTER_CWD_KEY, place],
|
|
512
530
|
[FOOTER_GIT_KEY, gitChip],
|
|
513
531
|
]);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* footer 的会话累计 token:统计 `sessionManager.getEntries()` 里每条条目的 usage。
|
|
3
|
+
*
|
|
4
|
+
* 口径是 `usage.totalTokens` 累加(输入 + 输出 + 缓存读 + 缓存写),
|
|
5
|
+
* 覆盖 assistant / toolResult 消息,以及 compaction、branch_summary 与 `usage` 条目
|
|
6
|
+
* 自己那次 LLM 调用的开销。整个会话全量累加,不沿当前分支裁剪:
|
|
7
|
+
* 回退/重发放弃的节点也算进「这次对话烧掉了多少 token」。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { formatTokenCount } from "../../utils/format.ts";
|
|
11
|
+
|
|
12
|
+
type UnknownRecord = Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
function asRecord(value: unknown): UnknownRecord | undefined {
|
|
15
|
+
return value && typeof value === "object" ? (value as UnknownRecord) : undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** `usage.totalTokens` 优先;缺失时用四项相加,非数字一律忽略。 */
|
|
19
|
+
export function usageTokenTotal(usage: unknown): number {
|
|
20
|
+
const record = asRecord(usage);
|
|
21
|
+
if (!record) return 0;
|
|
22
|
+
const total = record.totalTokens;
|
|
23
|
+
if (typeof total === "number" && Number.isFinite(total) && total > 0) return total;
|
|
24
|
+
let sum = 0;
|
|
25
|
+
for (const key of ["input", "output", "cacheRead", "cacheWrite"] as const) {
|
|
26
|
+
const part = record[key];
|
|
27
|
+
if (typeof part === "number" && Number.isFinite(part) && part > 0) sum += part;
|
|
28
|
+
}
|
|
29
|
+
return sum;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 一条会话条目贡献的 token;user/system/自定义条目不消费 LLM token,返回 0。 */
|
|
33
|
+
export function sessionEntryTokens(entry: unknown): number {
|
|
34
|
+
const record = asRecord(entry);
|
|
35
|
+
if (!record) return 0;
|
|
36
|
+
if (record.type === "message") {
|
|
37
|
+
const message = asRecord(record.message);
|
|
38
|
+
if (!message) return 0;
|
|
39
|
+
if (message.role !== "assistant" && message.role !== "toolResult") return 0;
|
|
40
|
+
return usageTokenTotal(message.usage);
|
|
41
|
+
}
|
|
42
|
+
if (record.type === "compaction" || record.type === "branch_summary" || record.type === "usage") {
|
|
43
|
+
return usageTokenTotal(record.usage);
|
|
44
|
+
}
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 芯片文案:`∑ 402k`;没有数据返回空串(不占位置)。 */
|
|
49
|
+
export function formatSessionTokenTotal(total: number): string {
|
|
50
|
+
if (!Number.isFinite(total) || total <= 0) return "";
|
|
51
|
+
return `∑ ${formatTokenCount(Math.round(total))}`;
|
|
52
|
+
}
|