pi-cc-extensions 0.8.49 → 0.8.51
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/extensions/config/config.ts +1 -6
- package/extensions/config/panel.ts +1 -6
- package/extensions/feature/agent-summary.ts +34 -0
- package/extensions/feature/compact-thinking.ts +7 -7
- package/extensions/feature/markdown-enhance.ts +4 -1
- package/extensions/feature/pi-startup-header.ts +3 -8
- package/extensions/index.ts +6 -1
- package/extensions/renderer/index.ts +74 -6
- package/extensions/renderer/message-display.ts +177 -0
- package/extensions/renderer/show-more-hint.ts +1 -3
- package/extensions/renderer/tool-grouping.ts +2 -6
- package/extensions/renderer/tool-result.ts +12 -0
- package/extensions/utils/agent-summary.ts +189 -0
- package/package.json +3 -2
- package/themes/cc-dark.json +2 -2
- package/themes/cc-light.json +1 -1
|
@@ -163,12 +163,7 @@ export function normalizeConfig(input: unknown): Config {
|
|
|
163
163
|
DEFAULT_CONFIG.animationIntervalMs,
|
|
164
164
|
),
|
|
165
165
|
showStartupHeader: source.showStartupHeader !== false,
|
|
166
|
-
scrollStepLines: pickPositiveInt(
|
|
167
|
-
source.scrollStepLines,
|
|
168
|
-
DEFAULT_CONFIG.scrollStepLines,
|
|
169
|
-
1,
|
|
170
|
-
50,
|
|
171
|
-
),
|
|
166
|
+
scrollStepLines: pickPositiveInt(source.scrollStepLines, DEFAULT_CONFIG.scrollStepLines, 1, 50),
|
|
172
167
|
};
|
|
173
168
|
}
|
|
174
169
|
|
|
@@ -348,12 +348,7 @@ export async function showCcstylePanel(
|
|
|
348
348
|
applyStartupHeader(ctx);
|
|
349
349
|
break;
|
|
350
350
|
case "scrollStepLines":
|
|
351
|
-
config.scrollStepLines = pickPositiveInt(
|
|
352
|
-
value,
|
|
353
|
-
DEFAULT_CONFIG.scrollStepLines,
|
|
354
|
-
1,
|
|
355
|
-
50,
|
|
356
|
-
);
|
|
351
|
+
config.scrollStepLines = pickPositiveInt(value, DEFAULT_CONFIG.scrollStepLines, 1, 50);
|
|
357
352
|
break;
|
|
358
353
|
default:
|
|
359
354
|
return;
|
|
@@ -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>(AGENT_SUMMARY_ENTRY_TYPE, (entry, _options, theme) => {
|
|
23
|
+
const line = summaryMarkdown(entry.data, true, {
|
|
24
|
+
success: theme.getFgAnsi("success"),
|
|
25
|
+
failed: theme.getFgAnsi("error"),
|
|
26
|
+
});
|
|
27
|
+
if (!line) return undefined;
|
|
28
|
+
return new Markdown(line, 1, 0, getMarkdownTheme());
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
bindAgentSummary(pi, (data) => {
|
|
32
|
+
pi.appendEntry(AGENT_SUMMARY_ENTRY_TYPE, data);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -363,13 +363,13 @@ function compactThinking(pi: ExtensionAPI) {
|
|
|
363
363
|
const content = message.content[i];
|
|
364
364
|
|
|
365
365
|
if (content.type === "text" && content.text.trim()) {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
366
|
+
self.contentContainer.addChild(
|
|
367
|
+
new Markdown(content.text.trim(), self.outputPad, 0, self.markdownTheme, undefined, {
|
|
368
|
+
transform: createTransform("assistant", self),
|
|
369
|
+
}),
|
|
370
|
+
);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
373
|
|
|
374
374
|
if (content.type !== "thinking") continue;
|
|
375
375
|
|
|
@@ -170,7 +170,10 @@ function getArt(src: string): ReturnType<typeof renderMermaid> | null {
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
/** Mermaid 方言代码块 → ASCII 图(流式跳过)。 */
|
|
173
|
-
function renderDiagrams(
|
|
173
|
+
function renderDiagrams(
|
|
174
|
+
markdown: string,
|
|
175
|
+
context?: { isStreaming?: boolean; availableWidth?: number },
|
|
176
|
+
): string {
|
|
174
177
|
const { isStreaming = false, availableWidth } = context ?? {};
|
|
175
178
|
if (isStreaming) return markdown;
|
|
176
179
|
const lines = markdown.split("\n");
|
|
@@ -15,13 +15,7 @@ const ANSI_PATTERN =
|
|
|
15
15
|
|
|
16
16
|
// 官方 install.sh 静态 logo(4 行原样,短行补尾随空格统一到 8 列)+ 底部空行补到 5 行,
|
|
17
17
|
// 与右侧 tips 行数等高;着色保持现状(accent 渐变)
|
|
18
|
-
const LOGO_LINES = [
|
|
19
|
-
"██████ ",
|
|
20
|
-
"██ ██ ",
|
|
21
|
-
"████ ██",
|
|
22
|
-
"██ ██",
|
|
23
|
-
" ",
|
|
24
|
-
];
|
|
18
|
+
const LOGO_LINES = ["██████ ", "██ ██ ", "████ ██", "██ ██", " "];
|
|
25
19
|
|
|
26
20
|
// hero 文案(单行,替换原生 header 的 "Pi can explain..." 默认位置)
|
|
27
21
|
const HERO_PREFIX = "There are many agent harnesses, but this one is ";
|
|
@@ -325,7 +319,8 @@ export function renderHeaderLines(
|
|
|
325
319
|
];
|
|
326
320
|
|
|
327
321
|
return paddedLeft.map(
|
|
328
|
-
(line, index) =>
|
|
322
|
+
(line, index) =>
|
|
323
|
+
`${line}${" ".repeat(TWO_COL_GAP)}${fitLineToWidth(rightLines[index] ?? "", rightWidth)}`,
|
|
329
324
|
);
|
|
330
325
|
}
|
|
331
326
|
|
package/extensions/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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 = {
|
|
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,177 @@
|
|
|
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) =>
|
|
49
|
+
`Compacted from ${Number(component.message?.tokensBefore ?? 0).toLocaleString()} tokens`,
|
|
50
|
+
body: (component) => String(component.message?.summary ?? ""),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const BRANCH_KIND: DisplayKind = {
|
|
54
|
+
title: () => "Branch summary",
|
|
55
|
+
body: (component) => String(component.message?.summary ?? ""),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function renderCcstyle(component: any, kind: DisplayKind): void {
|
|
59
|
+
const theme = displayTheme;
|
|
60
|
+
if (!theme) return; // 主题未就绪时保留原生渲染
|
|
61
|
+
if (component.bgFn) component.setBgFn?.(undefined); // 与工具调用一致,去掉灰底
|
|
62
|
+
if (component.paddingY !== 0) {
|
|
63
|
+
component._ccstyleOriginalPaddingY = component.paddingY;
|
|
64
|
+
component.paddingY = 0; // 工具组件 paddingY=0;原生 Box 默认 1,会上下各留一个空行
|
|
65
|
+
}
|
|
66
|
+
component.clear();
|
|
67
|
+
const icon = `${BRIGHT_GREEN}✓${ANSI_FG_RESET}`; // 已完成消息,等同工具成功态
|
|
68
|
+
const title = theme.fg("toolTitle", kind.title(component));
|
|
69
|
+
if (!component.expanded) {
|
|
70
|
+
const hint = theme.fg("dim", ` • ${showMoreHintText()}`);
|
|
71
|
+
component.addChild(new Text(`${icon} ${title}${hint}`, 0, 0));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
component.addChild(new Text(`${icon} ${title}`, 0, 0));
|
|
75
|
+
component.addChild(new Spacer(1));
|
|
76
|
+
component.addChild(
|
|
77
|
+
new Markdown(kind.body(component), 0, 0, component.markdownTheme, {
|
|
78
|
+
color: (text: string) => theme.fg("customMessageText", text),
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
type PatchEntry = {
|
|
84
|
+
prototype: any;
|
|
85
|
+
installed: (...args: any[]) => void;
|
|
86
|
+
original: (...args: any[]) => void;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** patch 三个消息组件的 updateDisplay,返回统一 dispose(/reload 链安全)。 */
|
|
90
|
+
export function installMessageDisplayRendering(): () => void {
|
|
91
|
+
const host = globalThis as any;
|
|
92
|
+
const previous = host[MESSAGE_DISPLAY_PATCH];
|
|
93
|
+
if (previous) previous.dispose();
|
|
94
|
+
const patch: { active: boolean; entries: PatchEntry[]; dispose: () => void } = {
|
|
95
|
+
active: true,
|
|
96
|
+
entries: [],
|
|
97
|
+
dispose: () => {},
|
|
98
|
+
};
|
|
99
|
+
const installOne = (ComponentClass: any, kind: DisplayKind): void => {
|
|
100
|
+
const prototype = ComponentClass.prototype;
|
|
101
|
+
const original = prototype.updateDisplay;
|
|
102
|
+
const installed = function (this: any) {
|
|
103
|
+
if (patch.active && config.mode === "on") {
|
|
104
|
+
try {
|
|
105
|
+
renderCcstyle(this, kind);
|
|
106
|
+
return;
|
|
107
|
+
} catch {
|
|
108
|
+
// 渲染失败回退原生
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// 回退原生前恢复 paddingY,避免原生渲染丢失上下内边距
|
|
112
|
+
if (this._ccstyleOriginalPaddingY !== undefined) {
|
|
113
|
+
this.paddingY = this._ccstyleOriginalPaddingY;
|
|
114
|
+
delete this._ccstyleOriginalPaddingY;
|
|
115
|
+
}
|
|
116
|
+
original.call(this);
|
|
117
|
+
};
|
|
118
|
+
prototype.updateDisplay = installed;
|
|
119
|
+
patch.entries.push({ prototype, installed, original });
|
|
120
|
+
};
|
|
121
|
+
installOne(SkillInvocationMessageComponent, SKILL_KIND);
|
|
122
|
+
installOne(CompactionSummaryMessageComponent, COMPACTION_KIND);
|
|
123
|
+
installOne(BranchSummaryMessageComponent, BRANCH_KIND);
|
|
124
|
+
patch.dispose = () => {
|
|
125
|
+
patch.active = false;
|
|
126
|
+
for (const entry of patch.entries) {
|
|
127
|
+
if (entry.prototype.updateDisplay === entry.installed) {
|
|
128
|
+
entry.prototype.updateDisplay = entry.original;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (host[MESSAGE_DISPLAY_PATCH] === patch) delete host[MESSAGE_DISPLAY_PATCH];
|
|
132
|
+
};
|
|
133
|
+
host[MESSAGE_DISPLAY_PATCH] = patch;
|
|
134
|
+
return patch.dispose;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 三个消息组件共享的结构特征;CustomMessageComponent 用 rebuild 无 updateDisplay,不命中。 */
|
|
138
|
+
function isMessageDisplayComponent(value: any): boolean {
|
|
139
|
+
return Boolean(
|
|
140
|
+
value &&
|
|
141
|
+
typeof value === "object" &&
|
|
142
|
+
typeof value.markdownTheme === "object" &&
|
|
143
|
+
typeof value.setExpanded === "function" &&
|
|
144
|
+
typeof value.updateDisplay === "function" &&
|
|
145
|
+
typeof value.clear === "function",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** 遍历当前 transcript,让已挂载的消息组件按当前 mode 重渲染(/ccstyle on|off 切换)。 */
|
|
150
|
+
export function refreshMessageDisplays(root: any): void {
|
|
151
|
+
const seen = new Set<any>();
|
|
152
|
+
const visit = (value: any): void => {
|
|
153
|
+
if (!value || typeof value !== "object" || seen.has(value)) return;
|
|
154
|
+
seen.add(value);
|
|
155
|
+
if (Array.isArray(value)) {
|
|
156
|
+
for (const child of value) visit(child);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (isMessageDisplayComponent(value)) {
|
|
160
|
+
value.invalidate?.();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const children = value.children;
|
|
164
|
+
if (Array.isArray(children)) {
|
|
165
|
+
for (const child of children) visit(child);
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
const mounted = value.getMountedRoots?.();
|
|
169
|
+
if (Array.isArray(mounted)) {
|
|
170
|
+
for (const root of mounted) visit(root);
|
|
171
|
+
}
|
|
172
|
+
} catch {
|
|
173
|
+
// 惰性 Proxy 可能暂时没有 mounted roots
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
visit(root);
|
|
177
|
+
}
|
|
@@ -34,7 +34,5 @@ export function expandShortcutText(): string {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export function showMoreHintText(): string {
|
|
37
|
-
return isToolTuiFullscreen()
|
|
38
|
-
? "click to show more"
|
|
39
|
-
: `${expandShortcutText()} to show more`;
|
|
37
|
+
return isToolTuiFullscreen() ? "click to show more" : `${expandShortcutText()} to show more`;
|
|
40
38
|
}
|
|
@@ -387,12 +387,8 @@ export class ToolGroupComponent extends Container {
|
|
|
387
387
|
}
|
|
388
388
|
}
|
|
389
389
|
if (this._expanded) {
|
|
390
|
-
|
|
391
|
-
|
|
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,189 @@
|
|
|
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
|
+
* `colors`:成功/失败计数的 ANSI 前缀,取自主题(`theme.getFgAnsi("success")` /
|
|
132
|
+
* `theme.getFgAnsi("error")`),缺省不染色。仅数字染色:成功计数的数字染
|
|
133
|
+
* success 色、failed 计数的数字染 error 色,文本与时长不染色。颜色 span 以
|
|
134
|
+
* `\x1b[0m` 复位,引用块渲染器会在复位后重贴引用样式,后续文本不受影响。
|
|
135
|
+
*/
|
|
136
|
+
export function summaryMarkdown(
|
|
137
|
+
data: AgentSummaryData,
|
|
138
|
+
box = false,
|
|
139
|
+
colors: { success: string; failed: string } = { success: "", failed: "" },
|
|
140
|
+
): string {
|
|
141
|
+
const parts = summaryParts(data);
|
|
142
|
+
if (parts.length === 0) return "";
|
|
143
|
+
// 仅首段动词大写(与纯文本版“句首大写”一致)
|
|
144
|
+
const capitalizeFirst = (part: string, first: boolean) => {
|
|
145
|
+
const verb = part.match(/^[a-z]+/)?.[0] ?? "";
|
|
146
|
+
return first && verb ? verb[0].toUpperCase() + verb.slice(1) + part.slice(verb.length) : part;
|
|
147
|
+
};
|
|
148
|
+
// 仅染数字:`Read 2 files` → `Read <色>2</色> files`
|
|
149
|
+
const paintNumber = (code: string, part: string) =>
|
|
150
|
+
code ? part.replace(/(\d+)/, `${code}$1\x1b[0m`) : part;
|
|
151
|
+
const text = parts
|
|
152
|
+
.map((part, index) => capitalizeFirst(part, index === 0))
|
|
153
|
+
.map((part) => paintNumber(part.endsWith("failed") ? colors.failed : colors.success, part))
|
|
154
|
+
.join(", ");
|
|
155
|
+
const duration = formatDuration(data.durationMs);
|
|
156
|
+
const line = duration ? `${text} · ${duration}` : text;
|
|
157
|
+
return box ? `> *${line}*` : `**${line}**`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 绑定 pi 事件到摘要统计:
|
|
162
|
+
* - agent_start 重置统计
|
|
163
|
+
* - tool_execution_start / tool_execution_end 累计
|
|
164
|
+
* - agent_end 时回调快照(工具数 < minToolCount 不回调:单工具行本身已是摘要)
|
|
165
|
+
*
|
|
166
|
+
* 返回取消函数:清除当前统计引用(pi.on 不支持解绑,取消后不再产生回调)。
|
|
167
|
+
*/
|
|
168
|
+
export function bindAgentSummary(
|
|
169
|
+
pi: ExtensionAPI,
|
|
170
|
+
onSummary: (data: AgentSummaryData) => void,
|
|
171
|
+
minToolCount = 2,
|
|
172
|
+
): () => void {
|
|
173
|
+
let summary = new AgentRunSummary();
|
|
174
|
+
pi.on("agent_start", async () => {
|
|
175
|
+
summary = new AgentRunSummary();
|
|
176
|
+
});
|
|
177
|
+
pi.on("tool_execution_start", async (event) => {
|
|
178
|
+
summary.recordToolStart(event.toolName, event.args);
|
|
179
|
+
});
|
|
180
|
+
pi.on("tool_execution_end", async (event) => {
|
|
181
|
+
summary.recordToolResult(event.isError === true);
|
|
182
|
+
});
|
|
183
|
+
pi.on("agent_end", async () => {
|
|
184
|
+
if (summary.toolCount >= minToolCount) onSummary(summary.snapshot());
|
|
185
|
+
});
|
|
186
|
+
return () => {
|
|
187
|
+
summary = new AgentRunSummary();
|
|
188
|
+
};
|
|
189
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cc-extensions",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.51",
|
|
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",
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@biomejs/biome": "2.5.5",
|
|
65
|
-
"@types/node": "^26.1.1"
|
|
65
|
+
"@types/node": "^26.1.1",
|
|
66
|
+
"typescript": "^5.9.3"
|
|
66
67
|
}
|
|
67
68
|
}
|
package/themes/cc-dark.json
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"selectedBg": "#252035",
|
|
25
25
|
"userMessageBg": "#1e1e2e",
|
|
26
26
|
"toolPendingBg": "#1b1924",
|
|
27
|
-
"toolSuccessBg": "#
|
|
27
|
+
"toolSuccessBg": "#1e1e2e",
|
|
28
28
|
"toolErrorBg": "#2a1b22",
|
|
29
29
|
"customMessageBg": "#211c30"
|
|
30
30
|
},
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"thinkingHigh": "accentBright",
|
|
81
81
|
"thinkingXhigh": "#4db9cd",
|
|
82
82
|
"thinkingMax": "cyan",
|
|
83
|
-
"bashMode": "
|
|
83
|
+
"bashMode": "green"
|
|
84
84
|
},
|
|
85
85
|
"export": {
|
|
86
86
|
"pageBg": "bg",
|