@herbertgao/pi-extensions 2026.10.5 → 2026.10.6
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 +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +5 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +22 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/core.ts +2 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +131 -28
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +2 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/subagent.ts +3 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer.ts +10 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/token-rate.ts +15 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/working-message.ts +2 -23
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +105 -55
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +23 -471
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +6 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +16 -83
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/sticky-prompt.ts +53 -5
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/ask-user-question.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/ccstyle-tool.ts +467 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/ansi-utils.ts +1 -24
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-edit-render.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-limits.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-text.ts +2 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-write-render.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/line-width-safety.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +53 -34
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/message-display.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/names.ts +41 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -862
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/tool-io.ts +735 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/tool-state.ts +138 -0
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +5 -4
- package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-dark.json +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-light.json +2 -2
- package/node_modules/@herbertgao/pi-subagents/package.json +2 -1
- package/node_modules/@herbertgao/sol-pi/README.md +3 -3
- package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +4 -4
- package/node_modules/@herbertgao/sol-pi/agents-install.md +3 -3
- package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +1 -1
- package/package.json +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/show-more-hint.ts +0 -48
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Aggregate installer for HerbertGao-maintained Pi extensions and pinned upstream
|
|
|
8
8
|
pi install npm:@herbertgao/pi-extensions
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Requires Node.js 24 or newer and Pi 1.0
|
|
11
|
+
Requires Node.js 24 or newer and Pi 1.2.0 or newer.
|
|
12
12
|
|
|
13
13
|
## Native MCP
|
|
14
14
|
|
|
@@ -46,7 +46,7 @@ Pi loads their extensions and skills through `node_modules/` paths inside one pa
|
|
|
46
46
|
|
|
47
47
|
The local `@herbertgao/resume-from` fork is retired. If it is installed separately, remove that package before upgrading the aggregate to avoid registering `/resume-from` twice. The aggregate now uses upstream `resume-from@0.4.1`; its v0.4.0 start-directory fix supersedes our local patch.
|
|
48
48
|
|
|
49
|
-
The locally customized CC and SoL-Pi packages now follow complete upstream baselines `pi-cc-extensions@1.0.
|
|
49
|
+
The locally customized CC and SoL-Pi packages now follow complete upstream baselines `pi-cc-extensions@1.0.3` and SoL-Pi main `e1a586af`, with local behavior replayed on top. Action Fusion and ccstyle write rich diffs coexist without disabling either feature. All four SoL-Pi features remain opt-in; its new non-persistent-session evidence archives remain on disk for consumers and require host or caller cleanup.
|
|
50
50
|
|
|
51
51
|
### Agency Agents
|
|
52
52
|
|
|
@@ -112,13 +112,12 @@ pi -e . # one run, does not edit settings.json
|
|
|
112
112
|
|
|
113
113
|
## Compatibility
|
|
114
114
|
|
|
115
|
-
- Node.js `>=24`; the monorepo uses Pi `1.0
|
|
115
|
+
- Node.js `>=24`; the monorepo uses Pi `1.2.0`, with published Pi peer versions kept at `*`.
|
|
116
116
|
|
|
117
117
|
## Recommended companions
|
|
118
118
|
|
|
119
119
|
| Extension | Purpose |
|
|
120
120
|
| ------------------------ | ----------------------------------------------------------- |
|
|
121
|
-
| `npm:@ff-labs/pi-fff` | FFF-powered fuzzy file and content search (fffind / ffgrep) |
|
|
122
121
|
| `npm:pi-web-access` | Web search, URL fetching, GitHub cloning, PDF/video parsing |
|
|
123
122
|
| `npm:@narumitw/pi-usage` | Current-account usage for Codex / Copilot / OpenRouter |
|
|
124
123
|
|
|
@@ -112,13 +112,12 @@ pi -e . # 只跑一次,不改 settings.json
|
|
|
112
112
|
|
|
113
113
|
## 兼容性
|
|
114
114
|
|
|
115
|
-
- Node.js `>=24`;monorepo 使用 Pi `1.0
|
|
115
|
+
- Node.js `>=24`;monorepo 使用 Pi `1.2.0`,发布包的 Pi peer 版本保持 `*`。
|
|
116
116
|
|
|
117
117
|
## 推荐搭配
|
|
118
118
|
|
|
119
119
|
| 扩展 | 用途 |
|
|
120
120
|
| ---------------------------------------- | ------------------------------------------------ |
|
|
121
|
-
| `npm:@ff-labs/pi-fff` | 模糊文件与内容检索(fffind / ffgrep) |
|
|
122
121
|
| `npm:pi-web-access` | 网页搜索、URL 抓取、GitHub 克隆、PDF/视频解析 |
|
|
123
122
|
| `npm:@narumitw/pi-usage` | 查看当前账号用量(Codex / Copilot / OpenRouter) |
|
|
124
123
|
|
|
@@ -79,6 +79,8 @@ export type Config = {
|
|
|
79
79
|
footerTokenRate: boolean;
|
|
80
80
|
/** average:整段指令墙钟平均;live:最近 1 秒瞬时速率,空档回退平均。 */
|
|
81
81
|
footerTokenRateMode: FooterTokenRateMode;
|
|
82
|
+
/** 速率芯片是否带 TTFT。关则只显示速率。 */
|
|
83
|
+
footerTokenRateTtft: boolean;
|
|
82
84
|
/** ISO 4217 显示币种。USD 时不换算。 */
|
|
83
85
|
footerCurrency: string;
|
|
84
86
|
/** 1 单位费用对应的显示币种数量。null 只换符号、不乘倍率。 */
|
|
@@ -171,6 +173,7 @@ export const DEFAULT_CONFIG: Config = {
|
|
|
171
173
|
enableCustomFooter: true,
|
|
172
174
|
footerTokenRate: true,
|
|
173
175
|
footerTokenRateMode: "average",
|
|
176
|
+
footerTokenRateTtft: true,
|
|
174
177
|
footerCurrency: "USD",
|
|
175
178
|
footerCurrencyRate: null,
|
|
176
179
|
footerNerdIcons: true,
|
|
@@ -307,6 +310,7 @@ export function normalizeConfig(input: unknown): Config {
|
|
|
307
310
|
FOOTER_TOKEN_RATE_MODES,
|
|
308
311
|
DEFAULT_CONFIG.footerTokenRateMode,
|
|
309
312
|
),
|
|
313
|
+
footerTokenRateTtft: source.footerTokenRateTtft !== false,
|
|
310
314
|
footerCurrency: normalizeCurrency(source.footerCurrency),
|
|
311
315
|
footerCurrencyRate: normalizeFooterCurrencyRate(source.footerCurrencyRate),
|
|
312
316
|
footerNerdIcons: source.footerNerdIcons !== false,
|
|
@@ -372,6 +376,7 @@ export function formatConfigStatus(source: Config = config): string {
|
|
|
372
376
|
`footer=${source.enableCustomFooter ? "on" : "off"}`,
|
|
373
377
|
`footerTokenRate=${source.footerTokenRate ? "on" : "off"}`,
|
|
374
378
|
`footerTokenRateMode=${source.footerTokenRateMode}`,
|
|
379
|
+
`footerTokenRateTtft=${source.footerTokenRateTtft ? "on" : "off"}`,
|
|
375
380
|
`footerCurrency=${source.footerCurrency}`,
|
|
376
381
|
`footerRate=${source.footerCurrencyRate ?? "-"}`,
|
|
377
382
|
`footerIcons=${source.footerNerdIcons ? "nerd" : "plain"}`,
|
|
@@ -101,6 +101,12 @@ function footerTokenRateModeDescription(mode: FooterTokenRateMode): string {
|
|
|
101
101
|
: "Average over the whole user prompt (avg), including tool waits. Stays until the next prompt.";
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
function footerTokenRateTtftDescription(enabled: boolean): string {
|
|
105
|
+
return enabled
|
|
106
|
+
? "Append time to first token, e.g. (TTFT: 812ms) or (TTFT: 1.2s). Turn off to show the rate only."
|
|
107
|
+
: "Time to first token hidden. The rate chip shows tok/s only.";
|
|
108
|
+
}
|
|
109
|
+
|
|
104
110
|
function footerGitStatsDescription(mode: FooterGitStatsMode): string {
|
|
105
111
|
return mode === "branch"
|
|
106
112
|
? "Net changes since the merge base, including uncommitted edits. Base: origin/HEAD, then local main/master. Untracked files excluded; unavailable stats hidden."
|
|
@@ -758,6 +764,13 @@ export async function showCcstylePanel(
|
|
|
758
764
|
currentValue: config.footerTokenRateMode,
|
|
759
765
|
values: FOOTER_TOKEN_RATE_MODES,
|
|
760
766
|
};
|
|
767
|
+
const footerTokenRateTtftSetting = {
|
|
768
|
+
id: "footerTokenRateTtft",
|
|
769
|
+
label: "Token rate TTFT",
|
|
770
|
+
description: footerTokenRateTtftDescription(config.footerTokenRateTtft),
|
|
771
|
+
currentValue: config.footerTokenRateTtft ? "on" : "off",
|
|
772
|
+
values: ["on", "off"],
|
|
773
|
+
};
|
|
761
774
|
const pluginChipsSetting = {
|
|
762
775
|
id: "footerPluginChips",
|
|
763
776
|
label: "Plugin chips",
|
|
@@ -847,6 +860,14 @@ export async function showCcstylePanel(
|
|
|
847
860
|
ctx.ui.notify(`Updated ${id}: ${value}`, "info");
|
|
848
861
|
return;
|
|
849
862
|
}
|
|
863
|
+
if (id === "footerTokenRateTtft") {
|
|
864
|
+
const enabled = value === "on";
|
|
865
|
+
updateConfig({ footerTokenRateTtft: enabled });
|
|
866
|
+
footerTokenRateTtftSetting.currentValue = enabled ? "on" : "off";
|
|
867
|
+
footerTokenRateTtftSetting.description = footerTokenRateTtftDescription(enabled);
|
|
868
|
+
ctx.ui.notify(`Updated ${id}: ${value}`, "info");
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
850
871
|
if (id === "footerNerdIcons") {
|
|
851
872
|
const enabled = value === "on";
|
|
852
873
|
updateConfig({ footerNerdIcons: enabled });
|
|
@@ -1067,6 +1088,7 @@ export async function showCcstylePanel(
|
|
|
1067
1088
|
customFooterSetting,
|
|
1068
1089
|
footerTokenRateSetting,
|
|
1069
1090
|
footerTokenRateModeSetting,
|
|
1091
|
+
footerTokenRateTtftSetting,
|
|
1070
1092
|
footerCurrencySetting,
|
|
1071
1093
|
footerCurrencyRateSetting,
|
|
1072
1094
|
footerNerdIconsSetting,
|
|
@@ -39,9 +39,8 @@ function nonEmptyString(value: unknown): value is string {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function toolPath(args?: Record<string, unknown> | null): string | undefined {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return nonEmptyString(args?.file_path) ? args.file_path : undefined;
|
|
42
|
+
const path = args?.path ?? args?.file_path;
|
|
43
|
+
return nonEmptyString(path) ? path : undefined;
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
/** 累积一次 agent 回合的工具统计;agent_start 时新建实例。 */
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import { Box, Markdown, Spacer, Text, type Component } from "@earendil-works/pi-tui";
|
|
21
21
|
// 展开卡背景是 ccstyle 的渲染约定,不随上游 CompactThinkingConfig 走,直接读全局配置。
|
|
22
22
|
import { config as styleConfig } from "../config/config.ts";
|
|
23
|
-
import { isToolTuiFullscreen } from "../renderer/tool/
|
|
23
|
+
import { isToolTuiFullscreen } from "../renderer/tool/names.ts";
|
|
24
24
|
import {
|
|
25
25
|
animateCompactThinkingText,
|
|
26
26
|
formatThoughtDuration,
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type ExtensionAPI,
|
|
4
4
|
type ExtensionCommandContext,
|
|
5
5
|
type ToolInfo,
|
|
6
|
+
copyToClipboard,
|
|
6
7
|
estimateTokens,
|
|
7
8
|
formatSkillsForPrompt,
|
|
8
9
|
getMarkdownTheme,
|
|
@@ -54,6 +55,30 @@ export function escCloseHitbox(bounds: DialogBounds): {
|
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
const COPY_LABEL = "[copy]";
|
|
59
|
+
const ESC_LABEL = "[esc]";
|
|
60
|
+
|
|
61
|
+
/** 测试可替换;生产使用 pi 的剪贴板实现。 */
|
|
62
|
+
let copyTextImpl: (text: string) => Promise<void> = (text) => copyToClipboard(text);
|
|
63
|
+
|
|
64
|
+
export function setPreviewCopyImpl(impl: ((text: string) => Promise<void>) | null): void {
|
|
65
|
+
copyTextImpl = impl ?? ((text) => copyToClipboard(text));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 1-based terminal hitbox of the [copy] button, one space left of [esc] on the same title row. */
|
|
69
|
+
export function copyButtonHitbox(bounds: DialogBounds): {
|
|
70
|
+
row: number;
|
|
71
|
+
startCol: number;
|
|
72
|
+
endCol: number;
|
|
73
|
+
} {
|
|
74
|
+
const esc = escCloseHitbox(bounds);
|
|
75
|
+
return {
|
|
76
|
+
row: esc.row,
|
|
77
|
+
startCol: esc.startCol - visibleWidth(COPY_LABEL) - 1,
|
|
78
|
+
endCol: esc.startCol - 2,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
57
82
|
let activeContextOverlays = 0;
|
|
58
83
|
|
|
59
84
|
/** fullscreen 输入包装用于把鼠标事件继续传给当前 context 主弹框或文本预览 overlay。 */
|
|
@@ -81,7 +106,9 @@ export async function showTextPreview(
|
|
|
81
106
|
let pageSize = 1;
|
|
82
107
|
let totalLines = 1;
|
|
83
108
|
let escHovered = false;
|
|
109
|
+
let copyHovered = false;
|
|
84
110
|
let escHitbox: { row: number; startCol: number; endCol: number } | undefined;
|
|
111
|
+
let copyHitbox: { row: number; startCol: number; endCol: number } | undefined;
|
|
85
112
|
let scrollbarHitbox:
|
|
86
113
|
| {
|
|
87
114
|
col: number;
|
|
@@ -108,6 +135,24 @@ export async function showTextPreview(
|
|
|
108
135
|
tui.requestRender();
|
|
109
136
|
};
|
|
110
137
|
|
|
138
|
+
const setCopyHovered = (hovered: boolean): void => {
|
|
139
|
+
if (hovered === copyHovered) return;
|
|
140
|
+
copyHovered = hovered;
|
|
141
|
+
tui.requestRender();
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const copyPreview = (): void => {
|
|
145
|
+
void copyTextImpl(content)
|
|
146
|
+
.then(() => {
|
|
147
|
+
const flash = (tui as { flash?: (message: string) => void }).flash;
|
|
148
|
+
if (typeof flash === "function") flash("Copied!");
|
|
149
|
+
else ctx.ui.notify("Copied preview to clipboard", "info");
|
|
150
|
+
})
|
|
151
|
+
.catch((error: unknown) => {
|
|
152
|
+
ctx.ui.notify(error instanceof Error ? error.message : "Copy failed", "error");
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
|
|
111
156
|
const dragScrollbarTo = (mouseRow: number): void => {
|
|
112
157
|
if (!scrollbarHitbox || scrollbarDragOffset === null) return;
|
|
113
158
|
const trackSize = scrollbarHitbox.endRow - scrollbarHitbox.startRow + 1;
|
|
@@ -151,7 +196,14 @@ export async function showTextPreview(
|
|
|
151
196
|
mouse.col >= escHitbox.startCol &&
|
|
152
197
|
mouse.col <= escHitbox.endCol,
|
|
153
198
|
);
|
|
199
|
+
const overCopy = Boolean(
|
|
200
|
+
copyHitbox &&
|
|
201
|
+
mouse.row === copyHitbox.row &&
|
|
202
|
+
mouse.col >= copyHitbox.startCol &&
|
|
203
|
+
mouse.col <= copyHitbox.endCol,
|
|
204
|
+
);
|
|
154
205
|
setEscHovered(overEsc);
|
|
206
|
+
setCopyHovered(overCopy);
|
|
155
207
|
const button = mouseBaseButton(mouse.code);
|
|
156
208
|
const motion = (mouse.code & 32) !== 0;
|
|
157
209
|
if (button === 0 && !motion) {
|
|
@@ -159,6 +211,10 @@ export async function showTextPreview(
|
|
|
159
211
|
done(undefined);
|
|
160
212
|
return;
|
|
161
213
|
}
|
|
214
|
+
if (overCopy) {
|
|
215
|
+
copyPreview();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
162
218
|
if (
|
|
163
219
|
scrollbarHitbox &&
|
|
164
220
|
mouse.col === scrollbarHitbox.col &&
|
|
@@ -187,7 +243,7 @@ export async function showTextPreview(
|
|
|
187
243
|
|
|
188
244
|
render(width: number) {
|
|
189
245
|
const inner = Math.max(1, width - 2);
|
|
190
|
-
const
|
|
246
|
+
const actionsWidth = visibleWidth(`${COPY_LABEL} ${ESC_LABEL}`);
|
|
191
247
|
const bodyInner = Math.max(1, inner - 1);
|
|
192
248
|
const bodyWidth = Math.max(1, bodyInner - 1);
|
|
193
249
|
const terminalHeight = Math.max(1, tui.terminal.rows);
|
|
@@ -205,6 +261,7 @@ export async function showTextPreview(
|
|
|
205
261
|
const overlayTop = 2 + Math.floor((availableHeight - viewportHeight) / 2);
|
|
206
262
|
const overlayLeft = Math.floor((Math.max(1, tui.terminal.columns) - width) / 2);
|
|
207
263
|
escHitbox = escCloseHitbox({ left: overlayLeft, top: overlayTop, width });
|
|
264
|
+
copyHitbox = copyButtonHitbox({ left: overlayLeft, top: overlayTop, width });
|
|
208
265
|
const visible = wrapped.slice(scrollOffset, scrollOffset + pageSize);
|
|
209
266
|
const border = (text: string) => theme.fg("border", text);
|
|
210
267
|
const scrollable = totalLines > pageSize;
|
|
@@ -237,11 +294,11 @@ export async function showTextPreview(
|
|
|
237
294
|
});
|
|
238
295
|
const start = totalLines === 0 ? 0 : scrollOffset + 1;
|
|
239
296
|
const end = Math.min(totalLines, scrollOffset + pageSize);
|
|
240
|
-
const status = `${start}-${end} / ${totalLines} lines · ↑↓ PgUp/PgDn Home/End · [esc] close`;
|
|
297
|
+
const status = `${start}-${end} / ${totalLines} lines · ↑↓ PgUp/PgDn Home/End · [copy] · [esc] close`;
|
|
241
298
|
|
|
242
299
|
return [
|
|
243
300
|
border(`╭${"─".repeat(inner)}╮`),
|
|
244
|
-
`${border("│")}${padLine(` ${theme.bold(theme.fg("accent", title))}`, inner -
|
|
301
|
+
`${border("│")}${padLine(` ${theme.bold(theme.fg("accent", title))}`, inner - actionsWidth)}${theme.fg(copyHovered ? "text" : "muted", COPY_LABEL)} ${theme.fg(escHovered ? "text" : "muted", ESC_LABEL)}${border("│")}`,
|
|
245
302
|
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
246
303
|
...bodyRows,
|
|
247
304
|
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
@@ -273,6 +330,27 @@ const tokenEstimate = (value: unknown): number => {
|
|
|
273
330
|
return Math.max(0, Math.ceil(text.length / 4));
|
|
274
331
|
};
|
|
275
332
|
|
|
333
|
+
/** system 消息对请求的贡献:正文 + 各 section 文本 + 声明的工具 schema,与 pi 的 estimateTokens 同口径。 */
|
|
334
|
+
function systemMessageTokens(message: {
|
|
335
|
+
content?: unknown;
|
|
336
|
+
sections?: Record<string, string | null> | null;
|
|
337
|
+
toolsAdded?: unknown;
|
|
338
|
+
toolsRemoved?: unknown;
|
|
339
|
+
}): number {
|
|
340
|
+
let chars = typeof message.content === "string" ? message.content.length : 0;
|
|
341
|
+
for (const section of Object.values(message.sections ?? {})) {
|
|
342
|
+
if (typeof section === "string") chars += section.length;
|
|
343
|
+
}
|
|
344
|
+
if (message.toolsAdded) chars += JSON.stringify(message.toolsAdded).length;
|
|
345
|
+
if (message.toolsRemoved) chars += JSON.stringify(message.toolsRemoved).length;
|
|
346
|
+
return Math.ceil(chars / 4);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** prompt 组装会把 CRLF 归一成 LF,匹配前对齐行尾。 */
|
|
350
|
+
function normalizeLineEndings(text: string): string {
|
|
351
|
+
return text.replace(/\r\n?/g, "\n");
|
|
352
|
+
}
|
|
353
|
+
|
|
276
354
|
/** 只统计确实嵌进 system prompt 的片段,避免源文件预览把占用加两遍。 */
|
|
277
355
|
function embeddedTokens(prompt: string, chunk: string): number {
|
|
278
356
|
if (!chunk || !prompt.includes(chunk)) return 0;
|
|
@@ -348,24 +426,24 @@ export function collectContextBreakdown(
|
|
|
348
426
|
allTools: ToolInfo[],
|
|
349
427
|
): ContextBreakdown {
|
|
350
428
|
const options = (ctx.getSystemPromptOptions?.() ?? {}) as BuildSystemPromptOptions;
|
|
351
|
-
const systemPrompt = typeof ctx.getSystemPrompt === "function" ? ctx.getSystemPrompt() : "";
|
|
352
429
|
const selectedTools = new Set(options.selectedTools ?? ["read", "bash", "edit", "write"]);
|
|
353
430
|
const toolDefinitionPreview: string[] = [];
|
|
354
431
|
const toolResultPreview: string[] = [];
|
|
355
432
|
const contextPreview: string[] = [];
|
|
433
|
+
const systemPromptPreview: string[] = [];
|
|
356
434
|
let toolDefinitionTokens = 0;
|
|
357
435
|
let toolResultTokens = 0;
|
|
358
436
|
let contextTokens = 0;
|
|
437
|
+
let systemTokens = 0;
|
|
359
438
|
|
|
360
439
|
const memoryPreview: string[] = [];
|
|
361
|
-
|
|
362
|
-
for (const file of
|
|
363
|
-
memoryTokens += embeddedTokens(systemPrompt, file.content);
|
|
440
|
+
const contextFiles = options.contextFiles ?? [];
|
|
441
|
+
for (const file of contextFiles) {
|
|
364
442
|
memoryPreview.push(`## ${file.path}\n\n${previewValue(file.content)}`);
|
|
365
443
|
}
|
|
366
444
|
|
|
367
|
-
const
|
|
368
|
-
const
|
|
445
|
+
const skills = options.skills ?? [];
|
|
446
|
+
const skillsText = formatSkillsForPrompt(skills).trim();
|
|
369
447
|
|
|
370
448
|
for (const tool of allTools) {
|
|
371
449
|
if (!selectedTools.has(tool.name)) continue;
|
|
@@ -378,21 +456,51 @@ export function collectContextBreakdown(
|
|
|
378
456
|
toolDefinitionPreview.push(`## Definition: ${tool.name}\n\n${previewValue(definition)}`);
|
|
379
457
|
}
|
|
380
458
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
459
|
+
let memoryTokens = 0;
|
|
460
|
+
let skillsTokens = 0;
|
|
461
|
+
for (const projected of ctx.sessionManager.buildSessionProjection().entries) {
|
|
462
|
+
const source = projected.sourceEntry;
|
|
463
|
+
for (const message of projected.messages) {
|
|
464
|
+
if (message.role === "system") {
|
|
465
|
+
systemTokens += systemMessageTokens(message);
|
|
466
|
+
// Memory / skills 按投影中的实际 section 文本切分,与 system prompt 增量互补、不重不漏。
|
|
467
|
+
const sections = (message.sections ?? {}) as Record<string, string | null>;
|
|
468
|
+
const projectContext =
|
|
469
|
+
typeof sections.project_context === "string"
|
|
470
|
+
? normalizeLineEndings(sections.project_context)
|
|
471
|
+
: "";
|
|
472
|
+
for (const file of contextFiles) {
|
|
473
|
+
const chunk = `<project_instructions path="${file.path}">\n${normalizeLineEndings(file.content)}\n</project_instructions>`;
|
|
474
|
+
memoryTokens += embeddedTokens(projectContext, chunk);
|
|
475
|
+
systemTokens -= embeddedTokens(projectContext, chunk);
|
|
476
|
+
}
|
|
477
|
+
const skillsSection =
|
|
478
|
+
typeof sections.skills === "string" ? normalizeLineEndings(sections.skills) : "";
|
|
479
|
+
skillsTokens += embeddedTokens(skillsSection, skillsText);
|
|
480
|
+
systemTokens -= embeddedTokens(skillsSection, skillsText);
|
|
481
|
+
systemPromptPreview.push(
|
|
482
|
+
(source.type === "compaction"
|
|
483
|
+
? "## System prompt snapshot at compaction\n\n"
|
|
484
|
+
: "## System prompt update\n\n") +
|
|
485
|
+
previewValue({
|
|
486
|
+
content: message.content,
|
|
487
|
+
sections: message.sections,
|
|
488
|
+
toolsAdded: (message.toolsAdded ?? []).map((tool: { name?: string }) => tool?.name),
|
|
489
|
+
toolsRemoved: (message.toolsRemoved ?? []).map(
|
|
490
|
+
(tool: { name?: string }) => tool?.name,
|
|
491
|
+
),
|
|
492
|
+
}),
|
|
493
|
+
);
|
|
494
|
+
} else if (message.role === "assistant") {
|
|
495
|
+
contextTokens += estimateTokens(message);
|
|
385
496
|
for (const block of message.content) {
|
|
386
497
|
if (block.type === "toolCall") {
|
|
387
|
-
contextTokens += tokenEstimate(block.name) + tokenEstimate(block.arguments);
|
|
388
498
|
contextPreview.push(
|
|
389
499
|
`## Assistant tool call: ${block.name}\n\n${previewValue(block.arguments)}`,
|
|
390
500
|
);
|
|
391
501
|
} else if (block.type === "text") {
|
|
392
|
-
contextTokens += tokenEstimate(block.text);
|
|
393
502
|
contextPreview.push(`## Assistant\n\n${block.text}`);
|
|
394
503
|
} else if (block.type === "thinking") {
|
|
395
|
-
contextTokens += tokenEstimate(block.thinking);
|
|
396
504
|
contextPreview.push(`## Assistant thinking\n\n${block.thinking}`);
|
|
397
505
|
}
|
|
398
506
|
}
|
|
@@ -402,8 +510,8 @@ export function collectContextBreakdown(
|
|
|
402
510
|
`## Result: ${message.toolName}\n\n${previewValue(message.content)}`,
|
|
403
511
|
);
|
|
404
512
|
} else if (message.role === "bashExecution") {
|
|
405
|
-
|
|
406
|
-
|
|
513
|
+
contextTokens += estimateTokens(message);
|
|
514
|
+
contextPreview.push(
|
|
407
515
|
`## Bash\n\nCommand:\n\n${previewValue(message.command)}\n\nOutput:\n\n${previewValue(message.output)}`,
|
|
408
516
|
);
|
|
409
517
|
} else if (message.role === "branchSummary" || message.role === "compactionSummary") {
|
|
@@ -413,18 +521,10 @@ export function collectContextBreakdown(
|
|
|
413
521
|
contextTokens += estimateTokens(message);
|
|
414
522
|
contextPreview.push(`## ${message.role}\n\n${previewValue(message.content)}`);
|
|
415
523
|
}
|
|
416
|
-
} else if (entry.type === "compaction" || entry.type === "branch_summary") {
|
|
417
|
-
contextTokens += tokenEstimate(entry.summary);
|
|
418
|
-
contextPreview.push(
|
|
419
|
-
`## ${entry.type === "compaction" ? "Compaction" : "Branch summary"}\n\n${entry.summary}`,
|
|
420
|
-
);
|
|
421
|
-
} else if (entry.type === "custom_message") {
|
|
422
|
-
contextTokens += tokenEstimate(entry.content);
|
|
423
|
-
contextPreview.push(`## Custom: ${entry.customType}\n\n${previewValue(entry.content)}`);
|
|
424
524
|
}
|
|
425
525
|
}
|
|
426
526
|
|
|
427
|
-
|
|
527
|
+
systemTokens = Math.max(0, systemTokens);
|
|
428
528
|
return {
|
|
429
529
|
parts: [
|
|
430
530
|
{ label: "System prompt", tokens: systemTokens, color: "accent" },
|
|
@@ -435,7 +535,10 @@ export function collectContextBreakdown(
|
|
|
435
535
|
{ label: "Context", tokens: contextTokens, color: "warning" },
|
|
436
536
|
] satisfies ContextPart[],
|
|
437
537
|
previews: {
|
|
438
|
-
systemPrompt:
|
|
538
|
+
systemPrompt:
|
|
539
|
+
systemPromptPreview.join("\n\n") ||
|
|
540
|
+
(typeof ctx.getSystemPrompt === "function" ? ctx.getSystemPrompt() : "") ||
|
|
541
|
+
"No system prompt.",
|
|
439
542
|
memoryFiles: memoryPreview.join("\n\n") || "No memory files in context.",
|
|
440
543
|
skills: skillsText || "No skills in context.",
|
|
441
544
|
tools: toolDefinitionPreview.join("\n\n") || "No active tool definitions.",
|
|
@@ -439,6 +439,8 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
|
|
|
439
439
|
});
|
|
440
440
|
|
|
441
441
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
442
|
+
// Pi 1.2.0 custom events have no prompt. Older hosts omit messageType.
|
|
443
|
+
if (!("prompt" in event)) return;
|
|
442
444
|
const referenceIds = extractSessionReferenceIds(event.prompt);
|
|
443
445
|
if (referenceIds.length === 0) return;
|
|
444
446
|
|
|
@@ -26,6 +26,7 @@ const MENTION_PATTERN = /(?:^|[\t ])@([^\s@]*)$/;
|
|
|
26
26
|
const SUBAGENT_REFERENCE_PATTERN = /(?:^|\s)@subagent:\[([^\]]+)](?![\w-])/g;
|
|
27
27
|
|
|
28
28
|
function getAgentDir(): string {
|
|
29
|
+
// PI_CODING_AGENT_DIR 本身就是 agent 目录(默认 ~/.pi/agent),不是家目录。
|
|
29
30
|
const override = process.env.PI_CODING_AGENT_DIR;
|
|
30
31
|
return join(override ? override : join(homedir(), ".pi", "agent"), "agents");
|
|
31
32
|
}
|
|
@@ -214,6 +215,8 @@ export default function agentAutocompleteExtension(pi: ExtensionAPI): void {
|
|
|
214
215
|
|
|
215
216
|
// Inject as a request message, not systemPrompt, so the cached prefix stays stable.
|
|
216
217
|
pi.on("before_agent_start", async (event, _ctx) => {
|
|
218
|
+
// Pi 1.2.0 custom events have no prompt. Older hosts omit messageType.
|
|
219
|
+
if (!("prompt" in event)) return;
|
|
217
220
|
const agents = getAgents();
|
|
218
221
|
if (agents.length === 0) return;
|
|
219
222
|
|
|
@@ -53,13 +53,16 @@ export function footerGlyphs(nerdIcons: boolean): { git: string; cache: string;
|
|
|
53
53
|
: { git: "", cache: "", mcp: "" };
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* 思考等级的填充圆标:等级越高填得越满。
|
|
58
|
+
* 全部取自同一圆径的几何圆,避开占位虚线圆和偏小的半圆。
|
|
59
|
+
*/
|
|
57
60
|
const EFFORT_SYMBOL: Record<string, string> = {
|
|
58
|
-
minimal: "\
|
|
59
|
-
low: "\
|
|
61
|
+
minimal: "\u25cb",
|
|
62
|
+
low: "\u25ce",
|
|
60
63
|
medium: "\u25d4",
|
|
61
|
-
high: "\
|
|
62
|
-
xhigh: "\
|
|
64
|
+
high: "\u25d5",
|
|
65
|
+
xhigh: "\u25c9",
|
|
63
66
|
max: "\u25cf",
|
|
64
67
|
};
|
|
65
68
|
|
|
@@ -72,7 +75,7 @@ export function modelChip(
|
|
|
72
75
|
): string {
|
|
73
76
|
const symbol = EFFORT_SYMBOL[level];
|
|
74
77
|
if (!symbol) return theme.fg("accent", modelLabel);
|
|
75
|
-
return `${theme.fg("accent", modelLabel)} ${thinkingColor(symbol
|
|
78
|
+
return `${theme.fg("accent", modelLabel)} ${thinkingColor(`${symbol} ${level}`)}`;
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
/** git 状态符号按职能分色:冲突/落后 error,修改 warning,未跟踪 muted,领先 success。 */
|
|
@@ -441,6 +444,7 @@ const createCustomFooterFactory =
|
|
|
441
444
|
displayTokenRate(config.footerTokenRateMode),
|
|
442
445
|
currentTtftMs(),
|
|
443
446
|
config.footerTokenRateMode,
|
|
447
|
+
config.footerTokenRateTtft,
|
|
444
448
|
)
|
|
445
449
|
: "";
|
|
446
450
|
const rateChip = rateLabel ? theme.fg("dim", rateLabel) : "";
|
|
@@ -182,11 +182,23 @@ export function currentTtftMs(): number {
|
|
|
182
182
|
return Math.max(0, firstTokenAt - runStartTime);
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
/**
|
|
186
|
-
export function
|
|
185
|
+
/** 不足 1 秒用毫秒,否则用一位小数的秒。 */
|
|
186
|
+
export function formatTtft(ttftMs: number): string {
|
|
187
|
+
if (ttftMs < 1000) return `${Math.round(ttftMs)}ms`;
|
|
188
|
+
return `${(ttftMs / 1000).toFixed(1)}s`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** `avg 1.2k tok/s (TTFT: 812ms)`;无数据显示空串,未出首 token 或 showTtft 为 false 时无括号段。 */
|
|
192
|
+
export function formatTokenRate(
|
|
193
|
+
rate: number,
|
|
194
|
+
ttftMs = 0,
|
|
195
|
+
mode: TokenRateMode = "average",
|
|
196
|
+
showTtft = true,
|
|
197
|
+
): string {
|
|
187
198
|
if (!Number.isFinite(rate) || rate <= 0) return "";
|
|
188
199
|
const value = rate >= 1000 ? `${(rate / 1000).toFixed(1)}k` : `${Math.round(rate)}`;
|
|
189
|
-
const ttft =
|
|
200
|
+
const ttft =
|
|
201
|
+
showTtft && Number.isFinite(ttftMs) && ttftMs > 0 ? ` (TTFT: ${formatTtft(ttftMs)})` : "";
|
|
190
202
|
const label = mode === "live" ? "live" : "avg";
|
|
191
203
|
return `${label} ${value} tok/s${ttft}`;
|
|
192
204
|
}
|
package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/working-message.ts
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { getCompactRunStatusText } from "../../renderer/compact-mode.ts";
|
|
3
|
-
import {
|
|
4
|
-
fullscreenLazyTui,
|
|
5
|
-
getToolMouseTui,
|
|
6
|
-
isFullscreenAtBottom,
|
|
7
|
-
} from "../../renderer/mouse/scroll.ts";
|
|
8
2
|
import { formatDuration, formatTokenCount } from "../../utils/format.ts";
|
|
9
3
|
import {
|
|
10
4
|
resetTokenRate,
|
|
@@ -60,25 +54,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
60
54
|
if (output > 0) recordProviderUsage(output);
|
|
61
55
|
}
|
|
62
56
|
|
|
63
|
-
/**
|
|
64
|
-
* 摘要行滚出视口才镜像:仅 fullscreen 且已离开 transcript 底部。
|
|
65
|
-
* regular 没有“离开底部”信号(transcript 在终端回滚区),保持 Pi 默认文案。
|
|
66
|
-
*/
|
|
67
|
-
function compactMirrorText(): string | undefined {
|
|
68
|
-
const tui = getToolMouseTui();
|
|
69
|
-
if (!tui || !fullscreenLazyTui(tui) || isFullscreenAtBottom(tui)) return undefined;
|
|
70
|
-
return getCompactRunStatusText();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
57
|
function buildWorkingMessage(): string {
|
|
74
|
-
// turn_end 后迟到的 provider 事件不得再驱动 footer(否则渲染出 epoch 级计时)。
|
|
75
|
-
if (!turnActive) return "";
|
|
76
58
|
const parts: string[] = [];
|
|
77
59
|
const tokens = tokenCount();
|
|
78
60
|
if (tokens > 0) parts.push(`↓ ${formatTokenCount(tokens)} tokens`);
|
|
79
|
-
// compact 活动回合且已滚出摘要行:直接用摘要行文案(自带回合时长,不叠 agent 计时)。
|
|
80
|
-
const compactStatus = compactMirrorText();
|
|
81
|
-
if (compactStatus) return [compactStatus, ...parts].join(" · ");
|
|
82
61
|
const elapsed = Date.now() - (agentStartTime || turnStartTime);
|
|
83
62
|
if (elapsed >= SHOW_TIMER_AFTER_MS || tokens > 0) {
|
|
84
63
|
// formatDuration 低于 1 秒返回 "",此处回退 "0s" 保持计时器连续跳动。
|
|
@@ -189,7 +168,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
189
168
|
evt.type === "toolcall_start"
|
|
190
169
|
) {
|
|
191
170
|
// TTFT 的首块含 toolcall_start:模型直接发起 tool call(thinking/text 省略)时也算首 token。
|
|
192
|
-
// 分段锚点只在 turn_start
|
|
171
|
+
// 分段锚点只在 turn_start 重置。这里再重置会把累计 usage.output 当成新的首报。
|
|
193
172
|
markFirstToken();
|
|
194
173
|
updateProviderUsage(evt.partial);
|
|
195
174
|
} else if (evt.type === "thinking_delta" || evt.type === "text_delta") {
|
|
@@ -200,7 +179,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
200
179
|
} else if (evt.type === "done") {
|
|
201
180
|
updateProviderUsage(evt.message);
|
|
202
181
|
} else if (evt.type === "error") {
|
|
203
|
-
|
|
182
|
+
updateProviderUsage(evt.error);
|
|
204
183
|
} else {
|
|
205
184
|
updateProviderUsage(evt.partial);
|
|
206
185
|
}
|