@herbertgao/pi-extensions 2026.10.4 → 2026.10.5

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 (30) hide show
  1. package/README.md +1 -1
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +12 -7
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +12 -7
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +67 -16
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +240 -48
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer-currency.ts +29 -0
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer.ts +92 -59
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/git-stats.ts +162 -0
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/token-rate.ts +192 -0
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/working-message.ts +51 -65
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +113 -25
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +160 -24
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +30 -1
  14. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +26 -24
  15. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/sticky-prompt.ts +402 -0
  16. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/ask-user-question.ts +173 -0
  17. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +4 -8
  18. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-edit-render.ts +64 -11
  19. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-presentation.ts +41 -0
  20. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +5 -1
  21. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-write-render.ts +35 -9
  22. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +101 -31
  23. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/names.ts +3 -3
  24. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +106 -15
  25. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/format.ts +9 -0
  26. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/patch-keys.ts +5 -0
  27. package/node_modules/@herbertgao/pi-cc-extensions/package.json +4 -4
  28. package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-dark.json +1 -1
  29. package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-light.json +1 -1
  30. package/package.json +2 -2
@@ -6,6 +6,7 @@ import { showMoreHintText } from "./show-more-hint.ts";
6
6
  import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
7
7
  import { getToolMouseTui } from "../mouse/scroll.ts";
8
8
  import { sanitizeToolResultText } from "../../utils/tool-result-sanitize.ts";
9
+ import { oneLine } from "../../utils/format.ts";
9
10
 
10
11
  const TOOL_VIEWPORT_WIDTH_RATIO = 0.8;
11
12
  /** 宽屏右侧留白上限:比例留白超过这么多列时改用固定留白。 */
@@ -75,10 +76,46 @@ export function countLines(text: string): number {
75
76
  .filter((line) => line.trim().length > 0).length;
76
77
  }
77
78
 
79
+ /**
80
+ * 折叠行的结果文案:`N lines returned`(read 说 loaded)、`Done`、失败时首行错误。
81
+ * 单卡折叠行与工具组的子行共用这一处口径;TaskList 的树形摘要不走这里,由调用方先判。
82
+ */
83
+ export function collapsedResultSummary(options: {
84
+ result: any;
85
+ toolName: string;
86
+ text: string;
87
+ isError: boolean;
88
+ }): string {
89
+ const { result, toolName, text, isError } = options;
90
+ if (isError) {
91
+ // 72 与原 result.ts 默认一致,保持错误摘要截断宽度。
92
+ return text ? oneLine(text, 72) : "Failed";
93
+ }
94
+ const outputLines = outputLineCount(result) || countLines(text);
95
+ if (!outputLines) return "Done";
96
+ const action = toolName === "read" ? "loaded" : "returned";
97
+ return `${outputLines} ${outputLines === 1 ? "line" : "lines"} ${action}`;
98
+ }
99
+
78
100
  function hasExpandableResult(text: string): boolean {
79
101
  return countLines(text) > 1;
80
102
  }
81
103
 
104
+ /**
105
+ * 工具耗时:不到 1 秒报毫秒,分钟级起带上分/时。
106
+ * 只报到秒的话,几分钟的长命令会显示成 `300.0s`。
107
+ */
108
+ export function formatToolDuration(ms: number): string {
109
+ const seconds = ms / 1000;
110
+ if (seconds < 1) return `${Math.round(ms)}ms`;
111
+ if (seconds < 60) return `${seconds.toFixed(1)}s`;
112
+ const totalSeconds = Math.floor(seconds);
113
+ const minutes = Math.floor(totalSeconds / 60);
114
+ const remainder = totalSeconds % 60;
115
+ if (minutes < 60) return `${minutes}m ${remainder}s`;
116
+ return `${Math.floor(minutes / 60)}h ${minutes % 60}m ${remainder}s`;
117
+ }
118
+
82
119
  const activeAnimationContexts = new Set<any>();
83
120
  let sharedAnimationTimer: ReturnType<typeof setTimeout> | null = null;
84
121
 
@@ -135,6 +172,36 @@ export function scheduleAnimation(
135
172
  }
136
173
  }
137
174
 
175
+ /**
176
+ * 本回合流式消息里出现过的工具调用。Pi 要等整条 assistant 消息结束才派发执行,
177
+ * 这段窗口工具行已经在等启动,spinner 该逐帧转。
178
+ * 恢复或翻历史重建的旧行不在这个集合里,保持静止。
179
+ */
180
+ const liveToolCalls = new Set<string>();
181
+
182
+ /** 收下当前流式 assistant 消息里的工具调用 id。 */
183
+ export function noteStreamingToolCalls(message: any): void {
184
+ const content = Array.isArray(message?.content) ? message.content : [];
185
+ for (const block of content) {
186
+ const id = block?.type === "toolCall" ? block.id : undefined;
187
+ if (typeof id === "string" && id) liveToolCalls.add(id);
188
+ }
189
+ }
190
+
191
+ /** 工具收尾后移出等待窗口。 */
192
+ export function retireLiveToolCall(toolCallId: unknown): void {
193
+ if (typeof toolCallId === "string") liveToolCalls.delete(toolCallId);
194
+ }
195
+
196
+ export function clearLiveToolCalls(): void {
197
+ liveToolCalls.clear();
198
+ }
199
+
200
+ /** 工具行是否处于等待窗口(流式声明中或等待派发)。 */
201
+ export function isLiveToolCall(toolCallId: unknown): boolean {
202
+ return typeof toolCallId === "string" && liveToolCalls.has(toolCallId);
203
+ }
204
+
138
205
  export function pendingIcon(_name: string): string {
139
206
  return toolLoadingIcon();
140
207
  }
@@ -231,7 +298,7 @@ export class ExpandedToolResultText {
231
298
  }
232
299
  }
233
300
 
234
- /** 截断体末行 `… +N more lines` 旁的展开提示,点击打开全量预览。 */
301
+ /** 截断体末行 `… N more lines` 旁的展开提示,点击打开全量预览。 */
235
302
  export const SHOW_MORE_LABEL = "· click to show more";
236
303
 
237
304
  export type ToolIoSection = "input" | "output";
@@ -243,14 +310,12 @@ const EXPANDED_TOOL_IO_VIEW_GENERATION = Symbol("ccstyle-expanded-tool-io-view")
243
310
  /**
244
311
  * Expanded tool body with clear Input / Output sections (Grok Build–style).
245
312
  *
246
- * Visual frame:
247
- * ├ Input click to show more
248
- * │ path: src/a.ts
249
- * │
250
- * └ Output click to show more
251
- * result line…
252
- *
253
- * flushLeft=true(仅 mode=on 展开卡):去掉树线前导空格,由外层 Box(1,1) 提供 1 格 padding。
313
+ * Visual frame(flushLeft=true:树线的前导空格交给外层 Box(1,1),├/└ 与标题行的勾同列):
314
+ * ├ Input click to show more
315
+ * │ path: src/a.ts
316
+ * │
317
+ * └ Output click to show more
318
+ * result line…
254
319
  *
255
320
  * Reused across re-renders via context.lastComponent when possible.
256
321
  */
@@ -281,8 +346,10 @@ export class ExpandedToolIoView {
281
346
  }
282
347
  | undefined;
283
348
 
284
- /** flushLeft:贴左渲染(mode=on 展开卡);默认 false 保留前导空格(compact 等共用路径)。 */
349
+ /** flushLeft:贴左渲染(外层 Box 负责 1 格 pad);默认 false 保留 1 格前导空格。 */
285
350
  private flushLeft: boolean;
351
+ /** 尾部附注(shell 的耗时)。挂到 Output 的 show-more 行上,没有那一行时单独成行。 */
352
+ private footer: string | undefined;
286
353
 
287
354
  constructor(
288
355
  theme: any,
@@ -398,6 +465,13 @@ export class ExpandedToolIoView {
398
465
  this.invalidate();
399
466
  }
400
467
 
468
+ /** 尾部附注(耗时等)。挂在 show-more 行上;无 show-more 行时单独成行。 */
469
+ setFooter(text: string | undefined): void {
470
+ if (this.footer === text) return;
471
+ this.footer = text;
472
+ this.invalidate();
473
+ }
474
+
401
475
  getHoveredSection(): ToolIoSection | null {
402
476
  return this.hoveredSection;
403
477
  }
@@ -405,7 +479,7 @@ export class ExpandedToolIoView {
405
479
  /** True when the plain truncation footer carries show-more. Input 续行带 │,Output 不带。 */
406
480
  matchShowMoreLine(plainLine: string): ToolIoSection | null {
407
481
  const line = plainLine.replace(/\x1b\[[0-9;]*m/g, "");
408
- if (!line.includes(` · ${showMoreHintText()}`) || !/\+\d+ more lines/.test(line)) return null;
482
+ if (!line.includes(` · ${showMoreHintText()}`) || !/\d+ more lines/.test(line)) return null;
409
483
  if (this.truncated.input && line.includes("│")) return "input";
410
484
  if (this.truncated.output) return "output";
411
485
  return null;
@@ -442,7 +516,7 @@ export class ExpandedToolIoView {
442
516
 
443
517
  const theme = this.theme;
444
518
  const safeWidth = Math.max(1, Math.floor(width));
445
- // flushLeft:贴左(外层 Box 负责 1 格 pad);否则保留 1 格前导空格(compact 共用)
519
+ // flushLeft:贴左(外层 Box 负责 1 格 pad);否则保留 1 格前导空格
446
520
  const lead = this.flushLeft ? "" : " ";
447
521
  const rail = `${lead}│ `;
448
522
  const railWidth = visibleWidth(rail);
@@ -474,7 +548,14 @@ export class ExpandedToolIoView {
474
548
 
475
549
  const pushBody = (
476
550
  body: string,
477
- opts: { input?: boolean; limit: number; continued?: boolean; section: ToolIoSection },
551
+ opts: {
552
+ input?: boolean;
553
+ limit: number;
554
+ continued?: boolean;
555
+ section: ToolIoSection;
556
+ /** 挂在 show-more 行上的附注(shell 耗时),插在展开提示前。 */
557
+ footerNote?: string;
558
+ },
478
559
  ): boolean /* truncated */ => {
479
560
  const raw = body.replace(/\t/g, " ").replace(/\n+$/, "");
480
561
  if (!raw.trim()) {
@@ -502,6 +583,8 @@ export class ExpandedToolIoView {
502
583
  if (truncated) {
503
584
  const hidden = Math.max(0, wrapped.length - visible.length);
504
585
  if (hidden > 0) {
586
+ // 省略号已经表达了「还有」,计数前不再加 +。
587
+ const note = opts.footerNote ? theme.fg("dim", ` · ${opts.footerNote}`) : "";
505
588
  // hover 只高亮文字,圆点保持 dim(与 group hint 一致)。
506
589
  const more =
507
590
  theme.fg("dim", " ·") +
@@ -509,7 +592,7 @@ export class ExpandedToolIoView {
509
592
  this.hoveredSection === opts.section ? "text" : "dim",
510
593
  ` ${showMoreHintText()}`,
511
594
  );
512
- pushRailLine(theme.fg("dim", `… +${hidden} more lines`) + more, opts.continued);
595
+ pushRailLine(theme.fg("dim", `… ${hidden} more lines`) + note + more, opts.continued);
513
596
  this.showMoreHeaderRows[opts.section] = lines.length - 1;
514
597
  }
515
598
  }
@@ -556,6 +639,7 @@ export class ExpandedToolIoView {
556
639
  limit: this.maxOutputLines,
557
640
  continued: false,
558
641
  section: "output",
642
+ footerNote: this.footer,
559
643
  });
560
644
  } else {
561
645
  this.truncated.output = outputWouldTruncate;
@@ -564,9 +648,16 @@ export class ExpandedToolIoView {
564
648
  limit: this.maxOutputLines,
565
649
  continued: false,
566
650
  section: "output",
651
+ footerNote: this.footer,
567
652
  });
568
653
  }
569
654
 
655
+ // 耗时挂在 Output 的 show-more 行上(与折叠态一样,提示留在行尾);
656
+ // 没截断就没有那一行,才单独成行。放在最后追加,前面记下的行号不受影响。
657
+ if (this.footer && this.showMoreHeaderRows.output === undefined) {
658
+ pushRailLine(theme.fg("muted", this.footer), false);
659
+ }
660
+
570
661
  this.cachedWidth = width;
571
662
  this.cachedLines = lines;
572
663
  return withIoViewMarkers(this, lines);
@@ -830,7 +921,7 @@ export function renderExpandedToolResult(
830
921
  lastComponent?: unknown,
831
922
  args?: unknown,
832
923
  context?: any,
833
- /** mode=on 展开卡贴左;compact 等保持默认前导空格 */
924
+ /** 贴左渲染:树线的前导空格交给外层 Box(1,1) */
834
925
  flushLeft = false,
835
926
  ): ExpandedToolIoView | ExpandedToolResultText | Text {
836
927
  const { text: inputBody, codeBlock } = formatToolInputBody(args);
@@ -1,6 +1,15 @@
1
1
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
2
  import { sanitizeToolResultText } from "./tool-result-sanitize.ts";
3
3
 
4
+ /** token/计数缩写:<1k 原数,<1M 用 k,≥1M 用 M。footer 与 working message 共用。 */
5
+ export function formatTokenCount(value: number): string {
6
+ if (value < 1000) return `${value}`;
7
+ if (value < 10_000) return `${(value / 1000).toFixed(1)}k`;
8
+ if (value < 1_000_000) return `${Math.round(value / 1000)}k`;
9
+ if (value < 10_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
10
+ return `${Math.round(value / 1_000_000)}M`;
11
+ }
12
+
4
13
  /** 毫秒 → "1h 2m 3s"/"2m 3s"/"3s";低于 1 秒返回 ""(省略)。 */
5
14
  export function formatDuration(ms: number): string {
6
15
  const totalSec = Math.floor(ms / 1000);
@@ -43,6 +43,11 @@ export const TOOL_GROUPING_GENERATION_KEY = Symbol.for("pi.ccstyle.tool-grouping
43
43
  // ── regular 主屏渲染补丁 ──
44
44
  export const MAIN_SCREEN_DO_RENDER_PATCH = Symbol.for("pi.ccstyle.main-screen-do-render-patch");
45
45
 
46
+ // ── fullscreen 粘性用户消息头 ──
47
+ export const STICKY_PROMPT_MARKER_PATCH = Symbol.for("pi.ccstyle.sticky-prompt-marker-patch");
48
+ export const STICKY_PROMPT_FRAME_PATCH = Symbol.for("pi.ccstyle.sticky-prompt-frame-patch");
49
+ export const STICKY_PROMPT_STATE_SLOT = Symbol.for("pi.ccstyle.sticky-prompt-state");
50
+
46
51
  // ── 鼠标交互 ──
47
52
  export const TOOL_MOUSE_OWNER_KEY = Symbol.for("pi.ccstyle.tool-mouse-owner");
48
53
  export const TOOL_MOUSE_TUI_SLOT = Symbol.for("pi.ccstyle.tool-mouse-tui");
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herbertgao/pi-cc-extensions",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "license": "MIT",
5
5
  "description": "A Pi productivity suite with Claude Code-style UI, context inspection, and agent/session references",
6
6
  "type": "module",
@@ -71,9 +71,9 @@
71
71
  },
72
72
  "x-upstream": {
73
73
  "package": "pi-cc-extensions",
74
- "version": "0.9.10",
75
- "reviewedVersion": "0.9.12",
74
+ "version": "1.0.1",
75
+ "reviewedVersion": "1.0.1",
76
76
  "repository": "https://github.com/minuque/pi-cc-extensions",
77
- "commit": "2b72669cafefe0771b2e3e63b0db5c478314d2d4"
77
+ "commit": "0188b76f2597772bea3b42075857e030d9a89666"
78
78
  }
79
79
  }
@@ -18,7 +18,7 @@
18
18
  "red": "#fb7185",
19
19
  "yellow": "#facc15",
20
20
  "orange": "#fb923c",
21
- "text": "#f5f5f5",
21
+ "text": "#e4e4e7",
22
22
  "muted": "#a1a1aa",
23
23
  "dim": "#71717a",
24
24
  "selectedBg": "#252035",
@@ -18,7 +18,7 @@
18
18
  "red": "#dc2626",
19
19
  "yellow": "#a16207",
20
20
  "orange": "#c2410c",
21
- "text": "#242127",
21
+ "text": "#282530",
22
22
  "muted": "#625f68",
23
23
  "dim": "#76717d",
24
24
  "selectedBg": "#e3e8f5",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herbertgao/pi-extensions",
3
- "version": "2026.10.4",
3
+ "version": "2026.10.5",
4
4
  "description": "Aggregate installer for HerbertGao-maintained Pi extensions.",
5
5
  "keywords": [
6
6
  "extensions",
@@ -40,7 +40,7 @@
40
40
  "@dietrichgebert/ponytail": "5.1.0",
41
41
  "@herbertgao/pi-agency": "0.1.0",
42
42
  "@herbertgao/pi-bark": "0.2.0",
43
- "@herbertgao/pi-cc-extensions": "0.11.1",
43
+ "@herbertgao/pi-cc-extensions": "0.12.0",
44
44
  "@herbertgao/pi-subagents": "0.19.0",
45
45
  "@herbertgao/sol-pi": "0.4.0",
46
46
  "@juicesharp/rpiv-ask-user-question": "2.12.0",