pi-cc-extensions 0.8.49 → 0.8.50

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.
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Agent 回合摘要展示:agent_end 时把本回合工具统计写入会话条目,
3
+ * 由 entry renderer 渲染为 markdown 引用块 `> [!TIP] *斜体内容*`。
4
+ *
5
+ * 统计复用 utils/agent-summary.ts(口径同旧 compact-style,ea64df0 删除)。
6
+ * appendEntry 不进 LLM 上下文,只显示在聊天区。
7
+ * 引用块文字色取主题 mdQuote(cc 主题下为 muted 灰),内容用 *斜体* 语法。
8
+ */
9
+
10
+ import { Markdown } from "@earendil-works/pi-tui";
11
+ import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
12
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
+ import {
14
+ bindAgentSummary,
15
+ summaryMarkdown,
16
+ type AgentSummaryData,
17
+ } from "../utils/agent-summary.ts";
18
+
19
+ export const AGENT_SUMMARY_ENTRY_TYPE = "agent-summary";
20
+
21
+ export default function (pi: ExtensionAPI): void {
22
+ pi.registerEntryRenderer<AgentSummaryData>(
23
+ AGENT_SUMMARY_ENTRY_TYPE,
24
+ (entry, _options, _theme) => {
25
+ const line = summaryMarkdown(entry.data, true);
26
+ if (!line) return undefined;
27
+ return new Markdown(line, 1, 0, getMarkdownTheme());
28
+ },
29
+ );
30
+
31
+ bindAgentSummary(pi, (data) => {
32
+ pi.appendEntry(AGENT_SUMMARY_ENTRY_TYPE, data);
33
+ });
34
+ }
@@ -1,7 +1,11 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import agentAutocomplete from "./feature/agent-autocomplete.ts";
3
+ import agentSummary from "./feature/agent-summary.ts";
3
4
  import claudeCodeStyle, { getCompactThinkingConfig } from "./renderer/index.ts";
4
- import { installCompactThinking, type CompactThinkingController } from "./feature/compact-thinking.ts";
5
+ import {
6
+ installCompactThinking,
7
+ type CompactThinkingController,
8
+ } from "./feature/compact-thinking.ts";
5
9
  import context from "./feature/context.ts";
6
10
  import markdownEnhance from "./feature/markdown-enhance.ts";
7
11
  import piAliases from "./feature/pi-aliases.ts";
@@ -25,4 +29,5 @@ export default function (pi: ExtensionAPI): void {
25
29
  context(pi);
26
30
  sessionReference(pi);
27
31
  agentAutocomplete(pi);
32
+ agentSummary(pi);
28
33
  }
@@ -32,6 +32,7 @@ import {
32
32
  clearAllAnimations,
33
33
  countLines,
34
34
  hasExpandableDetail,
35
+ headTruncateToWidth,
35
36
  isToolExpanded,
36
37
  middleTruncateToWidth,
37
38
  oneLine,
@@ -53,6 +54,12 @@ import {
53
54
  renderRichToolResult,
54
55
  WriteExecutionMetadataStore,
55
56
  } from "./tool-diff/index.ts";
57
+ import {
58
+ getMessageDisplayTheme,
59
+ installMessageDisplayRendering,
60
+ refreshMessageDisplays,
61
+ setMessageDisplayTheme,
62
+ } from "./message-display.ts";
56
63
 
57
64
  /**
58
65
  * Claude Code Style for pi — 装配入口(拆分后位于 ccstyle/index.ts)。
@@ -66,6 +73,7 @@ const ANSI_FG_RESET = "\x1b[39m";
66
73
 
67
74
  function refreshCurrentTranscript(ctx?: any, toolGrouping?: ToolGroupingHooks): void {
68
75
  toolGrouping?.refresh(toolMouseTui);
76
+ refreshMessageDisplays(toolMouseTui);
69
77
  toolMouseTui?.requestRender?.(true);
70
78
  ctx?.ui?.requestRender?.(true);
71
79
  }
@@ -133,10 +141,11 @@ function singleToolCallSummary(
133
141
  const name = toolName.toLowerCase();
134
142
  const value = (fallback: string, ...keys: string[]) => {
135
143
  const found = keys.map((key) => args[key]).find((item) => typeof item === "string" && item);
136
- return `${title} ${oneLine(found || fallback, Infinity)}`;
144
+ // 与多 tool 摘要对齐:从头截断,上限 96 字符(tool-grouping oneLine 默认值)。
145
+ return `${title} ${oneLine(found || fallback, 96)}`;
137
146
  };
138
147
  if (AGENT_FAMILY_TOOL_NAMES.has(toolName) && args.agent_id) {
139
- return { main: `${title} ${oneLine(args.agent_id, Infinity)}`, detail: "" };
148
+ return { main: `${title} ${oneLine(args.agent_id, 96)}`, detail: "" };
140
149
  }
141
150
  // Agents still uses the ccstyle wrapper; Agent keeps its dedicated renderer.
142
151
  if (name === "agents") {
@@ -177,7 +186,7 @@ function singleToolCallSummary(
177
186
  args.limit !== undefined ? `limit=${args.limit}` : "",
178
187
  ].filter(Boolean);
179
188
  return {
180
- main: `${title}${args.path ? ` ${oneLine(args.path, Infinity)}` : ""}`,
189
+ main: `${title}${args.path ? ` ${oneLine(args.path, 96)}` : ""}`,
181
190
  detail: details.length ? ` (${details.join(", ")})` : "",
182
191
  };
183
192
  }
@@ -197,7 +206,7 @@ function singleToolCallSummary(
197
206
  return {
198
207
  main:
199
208
  preferred !== undefined && preferred !== null && typeof preferred !== "object"
200
- ? `${title} ${oneLine(preferred, Infinity)}`
209
+ ? `${title} ${oneLine(preferred, 96)}`
201
210
  : title,
202
211
  detail: "",
203
212
  };
@@ -317,7 +326,8 @@ function createCcstyleTool(
317
326
  const callWidth = Math.max(0, viewportWidth - visibleWidth(icon) - 2);
318
327
  const mainWidth = Math.max(0, callWidth - visibleWidth(summary.detail));
319
328
  cachedWidth = width;
320
- cachedLine = ` ${icon} ${theme.fg("toolTitle", middleTruncateToWidth(summary.main, mainWidth))}${theme.fg("dim", summary.detail)}`;
329
+ // 纯文本先截断再着色(省略号不带 ANSI,避免 reset 破坏卡片背景);从头截断,与多 tool 一致
330
+ cachedLine = ` ${icon} ${theme.fg("toolTitle", headTruncateToWidth(summary.main, mainWidth))}${theme.fg("dim", summary.detail)}`;
321
331
  return [truncateToWidth(cachedLine, viewportWidth, "")];
322
332
  },
323
333
  invalidate() {},
@@ -738,6 +748,50 @@ function deactivateGlobalToolRendering(patch: GlobalToolRenderPatch): void {
738
748
 
739
749
  const GLOBAL_COMPACTION_RENDER_PATCH = Symbol.for("pi.ccstyle.compaction-render-patch");
740
750
 
751
+ const TOOL_EXPANDED_BACKGROUND_PATCH = Symbol.for("pi.ccstyle.tool-expanded-background-patch");
752
+
753
+ type ToolExpandedBackgroundPatch = {
754
+ active: boolean;
755
+ prototype: any;
756
+ installed: (...args: any[]) => void;
757
+ original: (...args: any[]) => void;
758
+ dispose: () => void;
759
+ };
760
+
761
+ /** 展开面板背景统一为 user message 背景色;折叠行保持原生状态色(由 original 重设)。 */
762
+ function installToolExpandedBackground(): () => void {
763
+ const host = globalThis as any;
764
+ const previous = host[TOOL_EXPANDED_BACKGROUND_PATCH] as ToolExpandedBackgroundPatch | undefined;
765
+ if (previous) previous.dispose();
766
+ const prototype = ToolExecutionComponent.prototype;
767
+ const original = prototype.updateDisplay;
768
+ const patch: ToolExpandedBackgroundPatch = {
769
+ active: true,
770
+ prototype,
771
+ installed: function (this: any) {
772
+ original.call(this);
773
+ if (!patch.active || config.mode !== "on" || !this.expanded) return;
774
+ const theme = getMessageDisplayTheme();
775
+ if (!theme?.bg) return;
776
+ const box = this.contentBox;
777
+ if (box?.setBgFn) box.setBgFn((text: string) => theme.bg("userMessageBg", text));
778
+ },
779
+ original,
780
+ dispose: () => {
781
+ patch.active = false;
782
+ if (prototype.updateDisplay === patch.installed) {
783
+ prototype.updateDisplay = original;
784
+ }
785
+ if (host[TOOL_EXPANDED_BACKGROUND_PATCH] === patch) {
786
+ delete host[TOOL_EXPANDED_BACKGROUND_PATCH];
787
+ }
788
+ },
789
+ };
790
+ prototype.updateDisplay = patch.installed;
791
+ host[TOOL_EXPANDED_BACKGROUND_PATCH] = patch;
792
+ return patch.dispose;
793
+ }
794
+
741
795
  type LegacyCompactionRenderPatch = {
742
796
  enabled?: () => boolean;
743
797
  };
@@ -763,6 +817,8 @@ export default function (
763
817
  | {
764
818
  globalToolRendering: GlobalToolRenderPatch;
765
819
  toolGrouping: ToolGroupingHooks;
820
+ disposeMessageDisplay: () => void;
821
+ disposeToolExpandedBackground: () => void;
766
822
  }
767
823
  | undefined;
768
824
  const ensureTuiInstallation = (ctx: any) => {
@@ -772,8 +828,15 @@ export default function (
772
828
  if (installation) return installation;
773
829
  const globalToolRendering = installGlobalToolRendering(writeExecutionMetadata);
774
830
  const toolGrouping = installToolGrouping(() => config.mode === "on");
831
+ const disposeMessageDisplay = installMessageDisplayRendering();
832
+ const disposeToolExpandedBackground = installToolExpandedBackground();
775
833
  deactivateLegacyCompactionRendering();
776
- installation = { globalToolRendering, toolGrouping };
834
+ installation = {
835
+ globalToolRendering,
836
+ toolGrouping,
837
+ disposeMessageDisplay,
838
+ disposeToolExpandedBackground,
839
+ };
777
840
  return installation;
778
841
  };
779
842
 
@@ -828,6 +891,7 @@ export default function (
828
891
  if (ctx?.mode === "tui" && ctx?.hasUI) installToolMouseInteraction(ctx, mouseOwner);
829
892
  if (!hooks) return;
830
893
  hooks.toolGrouping.setTheme(ctx.ui.theme);
894
+ setMessageDisplayTheme(ctx.ui.theme);
831
895
  ctx.ui.setStatus("ccstyle", undefined);
832
896
  scheduleSessionRender(() => hooks.toolGrouping.refresh(toolMouseTui));
833
897
  });
@@ -839,6 +903,7 @@ export default function (
839
903
  if (ctx?.mode === "tui" && ctx?.hasUI) installToolMouseInteraction(ctx, mouseOwner);
840
904
  if (!hooks) return;
841
905
  hooks.toolGrouping.setTheme(ctx.ui.theme);
906
+ setMessageDisplayTheme(ctx.ui.theme);
842
907
  scheduleSessionRender(() => hooks.toolGrouping.refresh(toolMouseTui));
843
908
  });
844
909
 
@@ -860,6 +925,8 @@ export default function (
860
925
  return;
861
926
  deactivateGlobalToolRendering(current.globalToolRendering);
862
927
  current.toolGrouping.shutdown();
928
+ current.disposeMessageDisplay();
929
+ current.disposeToolExpandedBackground();
863
930
  deactivateLegacyCompactionRendering();
864
931
  clearAllAnimations();
865
932
  installation = undefined;
@@ -880,6 +947,7 @@ export {
880
947
  ExpandedToolResultText,
881
948
  formatExpandHint,
882
949
  formatToolInputArgs,
950
+ headTruncateToWidth,
883
951
  middleTruncateToWidth,
884
952
  outputLineCount,
885
953
  renderCollapsedToolResult,
@@ -0,0 +1,163 @@
1
+ import {
2
+ BranchSummaryMessageComponent,
3
+ CompactionSummaryMessageComponent,
4
+ SkillInvocationMessageComponent,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import { Markdown, Spacer, Text } from "@earendil-works/pi-tui";
7
+ import { config } from "../config/config.ts";
8
+ import { showMoreHintText } from "./show-more-hint.ts";
9
+
10
+ /**
11
+ * 接管三个消息组件(`<skill>` 块、压缩摘要、分支摘要),ccstyle on 时渲染为
12
+ * 与工具调用一致的单行风格(● Title · hint),off 时回退原生。
13
+ *
14
+ * 三者结构同构:Box + markdownTheme + updateDisplay,构造/setExpanded/invalidate
15
+ * 三条路径都经过 updateDisplay,patch 一处即可全覆盖。它们不走
16
+ * ToolExecutionComponent,无法复用其原型 patch,这里单独统一管理。
17
+ */
18
+
19
+ const MESSAGE_DISPLAY_PATCH = Symbol.for("pi.ccstyle.message-display-patch");
20
+
21
+ // 与 renderer/index.ts renderCall 的成功勾一致:亮绿 ✓(truecolor ANSI)。
22
+ const BRIGHT_GREEN = "\x1b[38;2;80;220;100m";
23
+ const ANSI_FG_RESET = "\x1b[39m";
24
+
25
+ /** 事件上下文中同步的最新主题(与 toolGrouping.setTheme 同一来源)。 */
26
+ let displayTheme: any;
27
+
28
+ export function setMessageDisplayTheme(theme: any): void {
29
+ displayTheme = theme;
30
+ }
31
+
32
+ /** 供工具展开面板背景等共享同一主题来源(ctx.ui.theme)。 */
33
+ export function getMessageDisplayTheme(): any {
34
+ return displayTheme;
35
+ }
36
+
37
+ type DisplayKind = {
38
+ title(component: any): string;
39
+ body(component: any): string;
40
+ };
41
+
42
+ const SKILL_KIND: DisplayKind = {
43
+ title: (component) => `Skill ${String(component.skillBlock?.name ?? "skill")}`,
44
+ body: (component) => String(component.skillBlock?.content ?? ""),
45
+ };
46
+
47
+ const COMPACTION_KIND: DisplayKind = {
48
+ title: (component) => `Compacted from ${Number(component.message?.tokensBefore ?? 0).toLocaleString()} tokens`,
49
+ body: (component) => String(component.message?.summary ?? ""),
50
+ };
51
+
52
+ const BRANCH_KIND: DisplayKind = {
53
+ title: () => "Branch summary",
54
+ body: (component) => String(component.message?.summary ?? ""),
55
+ };
56
+
57
+ function renderCcstyle(component: any, kind: DisplayKind): void {
58
+ const theme = displayTheme;
59
+ if (!theme) return; // 主题未就绪时保留原生渲染
60
+ if (component.bgFn) component.setBgFn?.(undefined); // 与工具调用一致,去掉灰底
61
+ component.clear();
62
+ const icon = `${BRIGHT_GREEN}✓${ANSI_FG_RESET}`; // 已完成消息,等同工具成功态
63
+ const title = theme.fg("toolTitle", kind.title(component));
64
+ if (!component.expanded) {
65
+ const hint = theme.fg("dim", ` • ${showMoreHintText()}`);
66
+ component.addChild(new Text(`${icon} ${title}${hint}`, 0, 0));
67
+ return;
68
+ }
69
+ component.addChild(new Text(`${icon} ${title}`, 0, 0));
70
+ component.addChild(new Spacer(1));
71
+ component.addChild(
72
+ new Markdown(kind.body(component), 0, 0, component.markdownTheme, {
73
+ color: (text: string) => theme.fg("customMessageText", text),
74
+ }),
75
+ );
76
+ }
77
+
78
+ type PatchEntry = { prototype: any; installed: (...args: any[]) => void; original: (...args: any[]) => void };
79
+
80
+ /** patch 三个消息组件的 updateDisplay,返回统一 dispose(/reload 链安全)。 */
81
+ export function installMessageDisplayRendering(): () => void {
82
+ const host = globalThis as any;
83
+ const previous = host[MESSAGE_DISPLAY_PATCH];
84
+ if (previous) previous.dispose();
85
+ const patch: { active: boolean; entries: PatchEntry[]; dispose: () => void } = {
86
+ active: true,
87
+ entries: [],
88
+ dispose: () => {},
89
+ };
90
+ const installOne = (ComponentClass: any, kind: DisplayKind): void => {
91
+ const prototype = ComponentClass.prototype;
92
+ const original = prototype.updateDisplay;
93
+ const installed = function (this: any) {
94
+ if (patch.active && config.mode === "on") {
95
+ try {
96
+ renderCcstyle(this, kind);
97
+ return;
98
+ } catch {
99
+ // 渲染失败回退原生
100
+ }
101
+ }
102
+ original.call(this);
103
+ };
104
+ prototype.updateDisplay = installed;
105
+ patch.entries.push({ prototype, installed, original });
106
+ };
107
+ installOne(SkillInvocationMessageComponent, SKILL_KIND);
108
+ installOne(CompactionSummaryMessageComponent, COMPACTION_KIND);
109
+ installOne(BranchSummaryMessageComponent, BRANCH_KIND);
110
+ patch.dispose = () => {
111
+ patch.active = false;
112
+ for (const entry of patch.entries) {
113
+ if (entry.prototype.updateDisplay === entry.installed) {
114
+ entry.prototype.updateDisplay = entry.original;
115
+ }
116
+ }
117
+ if (host[MESSAGE_DISPLAY_PATCH] === patch) delete host[MESSAGE_DISPLAY_PATCH];
118
+ };
119
+ host[MESSAGE_DISPLAY_PATCH] = patch;
120
+ return patch.dispose;
121
+ }
122
+
123
+ /** 三个消息组件共享的结构特征;CustomMessageComponent 用 rebuild 无 updateDisplay,不命中。 */
124
+ function isMessageDisplayComponent(value: any): boolean {
125
+ return Boolean(
126
+ value &&
127
+ typeof value === "object" &&
128
+ typeof value.markdownTheme === "object" &&
129
+ typeof value.setExpanded === "function" &&
130
+ typeof value.updateDisplay === "function" &&
131
+ typeof value.clear === "function",
132
+ );
133
+ }
134
+
135
+ /** 遍历当前 transcript,让已挂载的消息组件按当前 mode 重渲染(/ccstyle on|off 切换)。 */
136
+ export function refreshMessageDisplays(root: any): void {
137
+ const seen = new Set<any>();
138
+ const visit = (value: any): void => {
139
+ if (!value || typeof value !== "object" || seen.has(value)) return;
140
+ seen.add(value);
141
+ if (Array.isArray(value)) {
142
+ for (const child of value) visit(child);
143
+ return;
144
+ }
145
+ if (isMessageDisplayComponent(value)) {
146
+ value.invalidate?.();
147
+ return;
148
+ }
149
+ const children = value.children;
150
+ if (Array.isArray(children)) {
151
+ for (const child of children) visit(child);
152
+ }
153
+ try {
154
+ const mounted = value.getMountedRoots?.();
155
+ if (Array.isArray(mounted)) {
156
+ for (const root of mounted) visit(root);
157
+ }
158
+ } catch {
159
+ // 惰性 Proxy 可能暂时没有 mounted roots
160
+ }
161
+ };
162
+ visit(root);
163
+ }
@@ -387,12 +387,8 @@ export class ToolGroupComponent extends Container {
387
387
  }
388
388
  }
389
389
  if (this._expanded) {
390
- const backgroundSlot =
391
- overall === "error"
392
- ? "toolErrorBg"
393
- : overall === "pending"
394
- ? "toolPendingBg"
395
- : "toolSuccessBg";
390
+ // 展开面板统一用 user message 背景色(ccstyle 约定),不按状态区分。
391
+ const backgroundSlot = "userMessageBg";
396
392
  for (const line of expandedLines) {
397
393
  lines.push(paddedBackgroundRow(theme, backgroundSlot, line, width));
398
394
  }
@@ -520,6 +520,18 @@ export function renderCollapsedToolResultToWidth(
520
520
  );
521
521
  }
522
522
 
523
+ /** 从头截断到宽度(保留开头、省略尾部),不插入 ANSI reset(背景卡片下安全)。 */
524
+ export function headTruncateToWidth(text: string, width: number): string {
525
+ if (visibleWidth(text) <= width) return text;
526
+ if (width <= 1) return "…";
527
+ let left = "";
528
+ for (const char of Array.from(text)) {
529
+ if (visibleWidth(left + "…" + char) > width) break;
530
+ left += char;
531
+ }
532
+ return `${left}…`;
533
+ }
534
+
523
535
  /** 截断到宽度时保留首尾内容(渲染折叠行与工具标题用)。 */
524
536
  export function middleTruncateToWidth(text: string, width: number): string {
525
537
  if (visibleWidth(text) <= width) return text;
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Agent 回合摘要:统计一次 agent 运行的工具使用并格式化成摘要文本。
3
+ *
4
+ * 统计口径参考已删除的 compact-style(ea64df0 "sunset fixed-editor and compact
5
+ * mode"):read 按文件去重、edit/write 按文件去重、bash 计命令数、其余工具计数、
6
+ * 失败计数、回合耗时。与旧实现一致,`read`/`edit`/`write` 只认精确的工具名
7
+ * (MCP 风格 `server__read` 归入 other)。
8
+ *
9
+ * 呈现:`summaryLine` 输出纯文本(旧格式),`summaryMarkdown` 输出 Markdown
10
+ * 增强版——加粗统计词,box 模式包 `> [!TIP]` 提示框(由 markdown-enhance 渲染
11
+ * 成带图标的表格框),供当前 Markdown 渲染管线使用。
12
+ */
13
+
14
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
+
16
+ /** 工具分类,与旧 compact-style 统计口径一致。 */
17
+ export type AgentToolCategory = "read" | "edit" | "bash" | "other";
18
+
19
+ /** 一次 agent 回合的统计快照。 */
20
+ export type AgentSummaryData = {
21
+ reads: number;
22
+ edits: number;
23
+ commands: number;
24
+ others: number;
25
+ failed: number;
26
+ durationMs: number;
27
+ };
28
+
29
+ export function classifyTool(toolName: string): AgentToolCategory {
30
+ const base = toolName.split(".").pop() ?? toolName;
31
+ if (base === "read") return "read";
32
+ if (base === "edit" || base === "write") return "edit";
33
+ if (base === "bash") return "bash";
34
+ return "other";
35
+ }
36
+
37
+ function nonEmptyString(value: unknown): value is string {
38
+ return typeof value === "string" && value.length > 0;
39
+ }
40
+
41
+ /** 累积一次 agent 回合的工具统计;agent_start 时新建实例。 */
42
+ export class AgentRunSummary {
43
+ toolCount = 0;
44
+ readFiles = new Set<string>();
45
+ editFiles = new Set<string>();
46
+ commandCount = 0;
47
+ otherCount = 0;
48
+ failedCount = 0;
49
+
50
+ readonly startedAt: number;
51
+
52
+ constructor(startedAt = Date.now()) {
53
+ this.startedAt = startedAt;
54
+ }
55
+
56
+ /** tool_execution_start 时调用。 */
57
+ recordToolStart(toolName: string, args?: Record<string, unknown> | null): void {
58
+ this.toolCount++;
59
+ const path = args?.path ?? args?.file_path;
60
+ switch (classifyTool(toolName)) {
61
+ case "read":
62
+ if (nonEmptyString(path)) this.readFiles.add(path);
63
+ break;
64
+ case "edit":
65
+ if (nonEmptyString(path)) this.editFiles.add(path);
66
+ break;
67
+ case "bash":
68
+ this.commandCount++;
69
+ break;
70
+ default:
71
+ this.otherCount++;
72
+ }
73
+ }
74
+
75
+ /** tool_execution_end 时调用;isError 为 true 计入失败。 */
76
+ recordToolResult(isError: boolean): void {
77
+ if (isError) this.failedCount++;
78
+ }
79
+
80
+ snapshot(now = Date.now()): AgentSummaryData {
81
+ return {
82
+ reads: this.readFiles.size,
83
+ edits: this.editFiles.size,
84
+ commands: this.commandCount,
85
+ others: this.otherCount,
86
+ failed: this.failedCount,
87
+ durationMs: now - this.startedAt,
88
+ };
89
+ }
90
+ }
91
+
92
+ /** 毫秒 → "1h 2m 3s"/"2m 3s"/"3s";低于 1 秒返回 ""(省略)。 */
93
+ export function formatDuration(ms: number): string {
94
+ const totalSec = Math.floor(ms / 1000);
95
+ if (totalSec < 1) return "";
96
+ const hours = Math.floor(totalSec / 3600);
97
+ const minutes = Math.floor((totalSec % 3600) / 60);
98
+ const seconds = totalSec % 60;
99
+ if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
100
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
101
+ return `${seconds}s`;
102
+ }
103
+
104
+ const plural = (count: number) => (count === 1 ? "" : "s");
105
+
106
+ function summaryParts(data: AgentSummaryData): string[] {
107
+ const parts: string[] = [];
108
+ if (data.reads) parts.push(`read ${data.reads} file${plural(data.reads)}`);
109
+ if (data.edits) parts.push(`edited ${data.edits} file${plural(data.edits)}`);
110
+ if (data.commands) parts.push(`ran ${data.commands} command${plural(data.commands)}`);
111
+ if (data.others) parts.push(`${data.others} other tool${plural(data.others)}`);
112
+ if (data.failed) parts.push(`${data.failed} failed`);
113
+ return parts;
114
+ }
115
+
116
+ /** 纯文本摘要行(旧 compact-style 格式):"Read 3 files, edited 2 files · 42s"。 */
117
+ export function summaryLine(data: AgentSummaryData): string {
118
+ const parts = summaryParts(data);
119
+ if (parts.length === 0) return "";
120
+ const text = parts.join(", ");
121
+ const capitalized = text[0].toUpperCase() + text.slice(1);
122
+ const duration = formatDuration(data.durationMs);
123
+ return duration ? `${capitalized} · ${duration}` : capitalized;
124
+ }
125
+
126
+ /**
127
+ * Markdown 摘要行。
128
+ * `box` 为 true:markdown 引用块 `> *斜体内容*`(渲染为引用,主题 mdQuote=muted 即灰色,
129
+ * 内容斜体)。entry renderer 场景直接用。
130
+ * `box` 为 false:整体加粗单行。
131
+ */
132
+ export function summaryMarkdown(data: AgentSummaryData, box = false): string {
133
+ const parts = summaryParts(data);
134
+ if (parts.length === 0) return "";
135
+ // 仅首段动词大写(与纯文本版“句首大写”一致)
136
+ const capitalizeFirst = (part: string, first: boolean) => {
137
+ const verb = part.match(/^[a-z]+/)?.[0] ?? "";
138
+ return first && verb ? verb[0].toUpperCase() + verb.slice(1) + part.slice(verb.length) : part;
139
+ };
140
+ const text = parts.map((part, index) => capitalizeFirst(part, index === 0)).join(", ");
141
+ const duration = formatDuration(data.durationMs);
142
+ const line = duration ? `${text} · ${duration}` : text;
143
+ return box ? `> *${line}*` : `**${line}**`;
144
+ }
145
+
146
+ /**
147
+ * 绑定 pi 事件到摘要统计:
148
+ * - agent_start 重置统计
149
+ * - tool_execution_start / tool_execution_end 累计
150
+ * - agent_end 时回调快照(工具数 < minToolCount 不回调:单工具行本身已是摘要)
151
+ *
152
+ * 返回取消函数:清除当前统计引用(pi.on 不支持解绑,取消后不再产生回调)。
153
+ */
154
+ export function bindAgentSummary(
155
+ pi: ExtensionAPI,
156
+ onSummary: (data: AgentSummaryData) => void,
157
+ minToolCount = 2,
158
+ ): () => void {
159
+ let summary = new AgentRunSummary();
160
+ pi.on("agent_start", async () => {
161
+ summary = new AgentRunSummary();
162
+ });
163
+ pi.on("tool_execution_start", async (event) => {
164
+ summary.recordToolStart(event.toolName, event.args);
165
+ });
166
+ pi.on("tool_execution_end", async (event) => {
167
+ summary.recordToolResult(event.isError === true);
168
+ });
169
+ pi.on("agent_end", async () => {
170
+ if (summary.toolCount >= minToolCount) onSummary(summary.snapshot());
171
+ });
172
+ return () => {
173
+ summary = new AgentRunSummary();
174
+ };
175
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cc-extensions",
3
- "version": "0.8.49",
3
+ "version": "0.8.50",
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",