@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.
Files changed (46) hide show
  1. package/README.md +2 -2
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -2
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +5 -0
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +22 -0
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/core.ts +2 -3
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +1 -1
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +131 -28
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +2 -0
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/subagent.ts +3 -0
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer.ts +10 -6
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/token-rate.ts +15 -3
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/working-message.ts +2 -23
  14. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +105 -55
  15. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +23 -471
  16. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +6 -3
  17. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +16 -83
  18. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +1 -1
  19. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +2 -2
  20. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +1 -1
  21. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/sticky-prompt.ts +53 -5
  22. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/ask-user-question.ts +1 -1
  23. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/ccstyle-tool.ts +467 -0
  24. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +1 -1
  25. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/ansi-utils.ts +1 -24
  26. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-edit-render.ts +1 -1
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-limits.ts +1 -1
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-text.ts +2 -1
  29. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-write-render.ts +1 -1
  30. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/line-width-safety.ts +1 -1
  31. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +53 -34
  32. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/message-display.ts +1 -1
  33. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/names.ts +41 -1
  34. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -862
  35. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/tool-io.ts +735 -0
  36. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/tool-state.ts +138 -0
  37. package/node_modules/@herbertgao/pi-cc-extensions/package.json +5 -4
  38. package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-dark.json +1 -1
  39. package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-light.json +2 -2
  40. package/node_modules/@herbertgao/pi-subagents/package.json +2 -1
  41. package/node_modules/@herbertgao/sol-pi/README.md +3 -3
  42. package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +4 -4
  43. package/node_modules/@herbertgao/sol-pi/agents-install.md +3 -3
  44. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +1 -1
  45. package/package.json +2 -2
  46. 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.4 or newer.
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.1` 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.
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.4`, with published Pi peer versions kept at `*`.
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.4`,发布包的 Pi peer 版本保持 `*`。
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
- // path 为空字符串时回退 file_path 别名,避免该文件不被计入去重集合。
43
- if (nonEmptyString(args?.path)) return args.path;
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/show-more-hint.ts";
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 escWidth = visibleWidth("[esc]");
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 - escWidth)}${theme.fg(escHovered ? "text" : "muted", "[esc]")}${border("│")}`,
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
- let memoryTokens = 0;
362
- for (const file of options.contextFiles ?? []) {
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 skillsText = formatSkillsForPrompt(options.skills ?? []).trim();
368
- const skillsTokens = embeddedTokens(systemPrompt, skillsText);
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
- for (const entry of ctx.sessionManager.buildContextEntries()) {
382
- if (entry.type === "message") {
383
- const message = entry.message;
384
- if (message.role === "assistant") {
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
- toolResultTokens += estimateTokens(message);
406
- toolResultPreview.push(
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
- const systemTokens = Math.max(0, tokenEstimate(systemPrompt) - memoryTokens - skillsTokens);
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: systemPrompt || "No system prompt.",
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: "\u25cc",
59
- low: "\u25cb",
61
+ minimal: "\u25cb",
62
+ low: "\u25ce",
60
63
  medium: "\u25d4",
61
- high: "\u25d1",
62
- xhigh: "\u25d5",
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)} ${theme.fg("dim", level)}`;
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
- /** `avg 1.2k tok/s (TTFT: 812ms)`;无数据显示空串,未出首 token 时无括号段。 */
186
- export function formatTokenRate(rate: number, ttftMs = 0, mode: TokenRateMode = "average"): string {
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 = Number.isFinite(ttftMs) && ttftMs > 0 ? ` (TTFT: ${Math.round(ttftMs)}ms)` : "";
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
  }
@@ -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 重置。这里再重置会把本段累计 usage.output 当成新的首报,live 速率翻倍。
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
- // provider 错误事件的 usage.output 不可信,不得计入 token 统计。
182
+ updateProviderUsage(evt.error);
204
183
  } else {
205
184
  updateProviderUsage(evt.partial);
206
185
  }