pi-extensions-tool-display 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,6 +8,8 @@ It provides:
8
8
  - a pending registration queue for when the Pi display host loads later;
9
9
  - safe component detection and a helper for appending an audit panel to the original tool result.
10
10
 
11
+ A result-render middleware registered for `"*"` really does run for every extension-registered tool: the host wires the middleware chain into any tool it does not already own, keeping the tool's own `renderResult` (or Pi's plain text preview when it has none) as the base. `isResultRenderPipelineActive` therefore reports `true` for those tools as well, so a consumer never needs to fall back to a separate transcript entry. Pi's own built-in tools stay behind the `registerToolOverrides` switches.
12
+
11
13
  It is also a standalone Pi extension. Install or include this package in Pi's package list to load the actual tool-display host. Feature package manifests include this dependency's extension entry, so installing either feature package loads one shared host without requiring a second host package.
12
14
 
13
15
  ## Boundary
package/README.zh-CN.md CHANGED
@@ -8,6 +8,8 @@
8
8
  - Pi 展示宿主尚未加载时的 pending 注册队列;
9
9
  - 安全识别组件,以及把审计面板追加到原始工具结果后的通用组件组合。
10
10
 
11
+ 用 `"*"` 注册的结果渲染 middleware 会真的作用到每个扩展注册的工具上:宿主会把这套中间件链路接进自己还没接管的工具,基线保留工具自带的 `renderResult`(没有自带渲染时复刻 Pi 的纯文本预览)。`isResultRenderPipelineActive` 对这些工具也会返回 `true`,调用方不需要再退回「往会话里追加独立 entry」的兜底显示。Pi 自己的内建工具仍由 `registerToolOverrides` 的开关控制。
12
+
11
13
  它同时是一个独立的 Pi 扩展。可以把这个包直接加入 Pi 的 package 列表加载工具展示宿主;`pi-distill`、`pi-tool-supervisor` 的包清单也会声明这个依赖的扩展入口,因此安装功能包时只会加载一个公共宿主,不需要额外安装第二份宿主包。
12
14
 
13
15
  ## 设计边界
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-extensions-tool-display",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "Pi tool display host and shared result-rendering protocol for extensions",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -48,7 +48,7 @@
48
48
  "tool-display"
49
49
  ],
50
50
  "dependencies": {
51
- "pi-extensions-i18n": "^0.6.0"
51
+ "pi-extensions-i18n": "^0.8.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@earendil-works/pi-coding-agent": ">=0.80.0",
@@ -12,6 +12,7 @@ import { shortenPath } from "./render-utils.js";
12
12
  import type { InspectorSettingItem } from "./settings-inspector-modal.js";
13
13
  import { type ToolDisplayConfig } from "./types.js";
14
14
  import {
15
+ NOTICE_TAG_COLOR,
15
16
  notifyWithSource,
16
17
  type NoticeColor,
17
18
  type NoticeLevel,
@@ -20,8 +21,8 @@ import {
20
21
 
21
22
  /** 本扩展的提示标签;短且唯一,便于在会话里定位来源。 */
22
23
  const NOTICE_TAG = "display";
23
- /** 提示标签颜色;与其它扩展错开,避免看起来像同一条消息。 */
24
- const NOTICE_COLOR: NoticeColor = "muted";
24
+ /** 提示标签颜色:所有扩展统一用弱化色,来源靠 tag 文本区分,不靠颜色。 */
25
+ const NOTICE_COLOR: NoticeColor = NOTICE_TAG_COLOR;
25
26
  /** 本扩展的提示来源。 */
26
27
  const NOTICE_SOURCE: NoticeSource = { tag: NOTICE_TAG, color: NOTICE_COLOR };
27
28
 
@@ -54,9 +54,9 @@ export function buildDiffSummaryText(stats: DiffSummaryStats, width: number): st
54
54
  }
55
55
 
56
56
  const summaryCandidates = [
57
- `↳ diff +${stats.added} -${stats.removed} • ${stats.hunks} ${pluralize(stats.hunks, "hunk")} • ${stats.files} ${pluralize(stats.files, "file")}`,
58
- `↳ diff +${stats.added} -${stats.removed} • ${stats.hunks}h • ${stats.files}f`,
59
- `↳ diff +${stats.added} -${stats.removed}`,
57
+ `diff +${stats.added} -${stats.removed} • ${stats.hunks} ${pluralize(stats.hunks, "hunk")} • ${stats.files} ${pluralize(stats.files, "file")}`,
58
+ `diff +${stats.added} -${stats.removed} • ${stats.hunks}h • ${stats.files}f`,
59
+ `diff +${stats.added} -${stats.removed}`,
60
60
  `+${stats.added} -${stats.removed}`,
61
61
  "diff",
62
62
  "…",
@@ -1871,7 +1871,7 @@ function renderDiffStatBar(stats: DiffStats, width: number, theme: DiffTheme): s
1871
1871
 
1872
1872
  function buildDiffSummaryBasePieces(stats: DiffStats, theme: DiffTheme): string[] {
1873
1873
  return [
1874
- theme.fg("toolOutput", `↳ ${emphasis(theme, "diff")}`),
1874
+ theme.fg("toolOutput", emphasis(theme, "diff")),
1875
1875
  theme.fg("toolDiffAdded", `+${stats.added}`),
1876
1876
  theme.fg("toolDiffRemoved", `-${stats.removed}`),
1877
1877
  ];
@@ -2074,7 +2074,7 @@ export function renderEditDiffResult(
2074
2074
  const diffText = safeGetDiff(details);
2075
2075
  if (!diffText.trim()) {
2076
2076
  if (!fallbackText.trim()) {
2077
- return new Text(theme.fg("muted", "↳ edit completed (no diff payload)"), 0, 0);
2077
+ return new Text(theme.fg("muted", "edit completed (no diff payload)"), 0, 0);
2078
2078
  }
2079
2079
  return new Text(theme.fg("toolOutput", fallbackText), 0, 0);
2080
2080
  }
@@ -2084,11 +2084,11 @@ export function renderEditDiffResult(
2084
2084
  parsed = parseDiff(diffText);
2085
2085
  } catch (error) {
2086
2086
  const message = error instanceof Error ? error.message : String(error);
2087
- return new Text(theme.fg("warning", `↳ unable to render diff: ${message}`), 0, 0);
2087
+ return new Text(theme.fg("warning", `unable to render diff: ${message}`), 0, 0);
2088
2088
  }
2089
2089
 
2090
2090
  if (parsed.entries.length === 0) {
2091
- return new Text(theme.fg("muted", "↳ no diff data"), 0, 0);
2091
+ return new Text(theme.fg("muted", "no diff data"), 0, 0);
2092
2092
  }
2093
2093
 
2094
2094
  const splitRows = buildSplitRows(parsed.entries);
@@ -2164,7 +2164,7 @@ function renderWriteHeader(
2164
2164
  ): string {
2165
2165
  const actionLabel = headerLabel?.trim() || (wasOverwrite ? "overwritten" : "created");
2166
2166
  return stabilizeBackgroundResets(
2167
- truncateToWidth(theme.fg("toolOutput", `↳ ${emphasis(theme, actionLabel)}`), width),
2167
+ truncateToWidth(theme.fg("toolOutput", emphasis(theme, actionLabel)), width),
2168
2168
  );
2169
2169
  }
2170
2170
 
@@ -2368,9 +2368,9 @@ function buildWriteOverwriteGuardText(guard: WriteOverwriteGuard, width: number)
2368
2368
  }
2369
2369
 
2370
2370
  const candidates = [
2371
- `↳ overwrite diff omitted (${guard.previousLineCount} → ${guard.nextLineCount} lines)`,
2372
- `↳ overwrite diff omitted (${guard.previousLineCount}→${guard.nextLineCount})`,
2373
- "↳ overwrite diff omitted",
2371
+ `overwrite diff omitted (${guard.previousLineCount} → ${guard.nextLineCount} lines)`,
2372
+ `overwrite diff omitted (${guard.previousLineCount}→${guard.nextLineCount})`,
2373
+ "overwrite diff omitted",
2374
2374
  "diff omitted",
2375
2375
  "…",
2376
2376
  ];
@@ -2399,7 +2399,7 @@ export function renderWriteDiffResult(
2399
2399
  ): Component {
2400
2400
  if (typeof content !== "string") {
2401
2401
  if (!fallbackText.trim()) {
2402
- return new Text(theme.fg("muted", "↳ write completed"), 0, 0);
2402
+ return new Text(theme.fg("muted", "write completed"), 0, 0);
2403
2403
  }
2404
2404
  return new Text(theme.fg("toolOutput", fallbackText), 0, 0);
2405
2405
  }
package/src/index.ts CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  type ToolDisplayConfig,
24
24
  } from "./types.js";
25
25
  import {
26
+ NOTICE_TAG_COLOR,
26
27
  installNoticeRenderer,
27
28
  notifyWithSource,
28
29
  type NoticeColor,
@@ -31,8 +32,8 @@ import {
31
32
 
32
33
  /** 本扩展的提示标签;短且唯一,便于在会话里定位来源。 */
33
34
  const NOTICE_TAG = "display";
34
- /** 提示标签颜色;与其它扩展错开,避免看起来像同一条消息。 */
35
- const NOTICE_COLOR: NoticeColor = "muted";
35
+ /** 提示标签颜色:所有扩展统一用弱化色,来源靠 tag 文本区分,不靠颜色。 */
36
+ const NOTICE_COLOR: NoticeColor = NOTICE_TAG_COLOR;
36
37
  /** 本扩展的提示来源。 */
37
38
  const NOTICE_SOURCE: NoticeSource = { tag: NOTICE_TAG, color: NOTICE_COLOR };
38
39
 
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 结果渲染中间件对工具行的覆盖。
3
+ *
4
+ * 中间件协议(`toolName: "*"`)的语义是「工具结果渲染时都会经过它」,但只有
5
+ * tool-display 自己装饰过的工具才会调用 `renderResultWithMiddleware`。没有被装饰的
6
+ * 工具(第三方扩展注册的工具、甚至没有 renderResult 的工具)拿不到中间件,调用方只
7
+ * 能退回「往会话里追加独立 entry」的兜底显示;这类 entry 长在工具行之外,折叠类扩展
8
+ * 收不到它,于是工具行收起了、审计行还留在原地。
9
+ *
10
+ * 这个模块提供把「被中间件命中、但我们还没接线的工具」补上接线所需的基线渲染:
11
+ * 工具自带 renderResult 时保留它,没有自带渲染时复刻 Pi 的默认结果块,接线前后观感
12
+ * 一致。
13
+ */
14
+
15
+ import { Text } from "@earendil-works/pi-tui";
16
+ import { sanitizeAnsiForThemedOutput } from "./ansi-utils.js";
17
+ import { extractTextOutput, previewLines } from "./render-utils.js";
18
+
19
+ interface RenderTheme {
20
+ fg(color: string, text: string): string;
21
+ }
22
+
23
+ interface ToolRenderResultOptions {
24
+ expanded?: boolean;
25
+ isPartial?: boolean;
26
+ }
27
+
28
+ /** Pi 在没有 renderResult 时只显示前 10 行;保持一致,接线后行数不变。 */
29
+ export const GENERIC_RESULT_PREVIEW_LINES = 10;
30
+
31
+ /**
32
+ * 复刻 Pi 的默认结果块:纯文本预览,折叠时超出部分给展开提示。
33
+ *
34
+ * 只用于没有自带 renderResult 的工具——它们本来就走 Pi 的这段渲染,这里复制一份是
35
+ * 为了让中间件有基线可挂,而不是改变这些工具的显示方式。
36
+ */
37
+ export function renderGenericResultPreview(
38
+ result: unknown,
39
+ options: ToolRenderResultOptions,
40
+ theme: RenderTheme,
41
+ ): Text {
42
+ const output = sanitizeAnsiForThemedOutput(extractTextOutput(result as never)).replace(/\r/g, "");
43
+ const lines = output.length > 0 ? output.split("\n") : [];
44
+ const maxLines = options.expanded === true ? lines.length : GENERIC_RESULT_PREVIEW_LINES;
45
+ const { shown, remaining } = previewLines(lines, maxLines);
46
+ let text = shown.map((line) => theme.fg("toolOutput", line)).join("\n");
47
+ if (remaining > 0) {
48
+ text += theme.fg("muted", `\n... (${remaining} more lines, Ctrl+O to expand)`);
49
+ }
50
+ return new Text(text, 0, 0);
51
+ }
@@ -2,6 +2,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
2
2
  import { isRecord } from "./tool-metadata.js";
3
3
  import { onReloadShutdown } from "./extension-lifecycle.js";
4
4
  import {
5
+ NOTICE_TAG_COLOR,
5
6
  notifyWithSource,
6
7
  type NoticeColor,
7
8
  type NoticeSource,
@@ -9,8 +10,8 @@ import {
9
10
 
10
11
  /** 本扩展的提示标签;短且唯一,便于在会话里定位来源。 */
11
12
  const NOTICE_TAG = "display";
12
- /** 提示标签颜色;与其它扩展错开,避免看起来像同一条消息。 */
13
- const NOTICE_COLOR: NoticeColor = "muted";
13
+ /** 提示标签颜色:所有扩展统一用弱化色,来源靠 tag 文本区分,不靠颜色。 */
14
+ const NOTICE_COLOR: NoticeColor = NOTICE_TAG_COLOR;
14
15
  /** 本扩展的提示来源。 */
15
16
  const NOTICE_SOURCE: NoticeSource = { tag: NOTICE_TAG, color: NOTICE_COLOR };
16
17
 
@@ -27,6 +27,7 @@ import { resolvePiAgentDir } from "./agent-dir.js";
27
27
  import { renderBashCall } from "./bash-display.js";
28
28
  import { logToolDisplayDebug } from "./debug-logger.js";
29
29
  import { registerCleanup } from "./disposable.js";
30
+ import { renderGenericResultPreview } from "./result-middleware-coverage.js";
30
31
  import {
31
32
  compactOutputLines,
32
33
  countNonEmptyLines,
@@ -67,6 +68,15 @@ import {
67
68
  shouldRenderWriteCallSummary,
68
69
  } from "./write-display-utils.js";
69
70
 
71
+ /**
72
+ * 结果摘要行(`loaded 42 lines`、`3 matches returned`、`(no output)`)不加前缀符号。
73
+ *
74
+ * `↳` 在会话里归活动区独占,表示「这条命令正在流出的输出尾巴」
75
+ * (见 pi-clean-mode 的 `activity.ts`)。两边都写 `↳` 时,同一个符号会在同一屏里
76
+ * 指两件事:一边是还没跑完的实时尾巴,一边是已经落定的结果计数。
77
+ * 摘要本身靠弱化色与「紧接在调用行下面」就能认出身份,不需要额外符号。
78
+ */
79
+
70
80
  interface BuiltInTools {
71
81
  read: ReturnType<typeof createReadTool>;
72
82
  grep: ReturnType<typeof createGrepTool>;
@@ -183,6 +193,8 @@ export interface ToolDisplayApi {
183
193
  registerResultRenderMiddleware(registration: ToolResultRenderMiddlewareRegistration): string;
184
194
  unregisterResultRenderMiddleware(id: string): boolean;
185
195
  hasResultRenderMiddleware(id: string): boolean;
196
+ /** 已注册中间件命中过的工具名,含通配符 `*`。 */
197
+ listResultRenderMiddlewareToolNames(): string[];
186
198
  activateResultRenderPipeline(toolName: string): void;
187
199
  isResultRenderPipelineActive(toolName: string): boolean;
188
200
  renderResultWithMiddleware(
@@ -238,6 +250,15 @@ type PiWithRegisterToolInterception = ExtensionAPI & {
238
250
  const decoratedToolDescriptors = new WeakMap<RuntimeToolDefinition, ToolPropertyDescriptorSnapshot>();
239
251
  const decoratedTools = new Set<RuntimeToolDefinition>();
240
252
 
253
+ /**
254
+ * 中间件注册之后的回调。
255
+ *
256
+ * 给工具补接线需要 pi 与当前工具列表,只有宿主拿得到,因此宿主在
257
+ * registerToolDisplayOverrides 里把自己那份实现挂上来;中间件在宿主安装之后才注册时
258
+ * (扩展加载顺序不固定),靠它立刻补上接线。
259
+ */
260
+ let resultRenderMiddlewareRegistrationListener: (() => void) | undefined;
261
+
241
262
  function registerRuntimeTool(pi: ExtensionAPI, tool: RuntimeToolDefinition): void {
242
263
  pi.registerTool(tool as unknown as ToolDefinition);
243
264
  }
@@ -435,7 +456,7 @@ function buildPreviewText(
435
456
  expanded: boolean,
436
457
  ): string {
437
458
  if (lines.length === 0) {
438
- return theme.fg("muted", "↳ (no output)");
459
+ return theme.fg("muted", "(no output)");
439
460
  }
440
461
 
441
462
  const { shown, remaining } = previewLines(lines, maxLines);
@@ -461,9 +482,9 @@ function formatBashNoOutputLine(
461
482
  theme: RenderTheme,
462
483
  ): string {
463
484
  if (isLikelyQuietCommand(command)) {
464
- return theme.fg("muted", "↳ command completed (no output)");
485
+ return theme.fg("muted", "command completed (no output)");
465
486
  }
466
- return theme.fg("muted", "↳ (no output)");
487
+ return theme.fg("muted", "(no output)");
467
488
  }
468
489
 
469
490
  function truncationHint(
@@ -946,7 +967,7 @@ function formatReadSummary(
946
967
  const lineCount = lines.length;
947
968
  let summary = theme.fg(
948
969
  "muted",
949
- `↳ loaded ${lineCount} ${pluralize(lineCount, "line")}`,
970
+ `loaded ${lineCount} ${pluralize(lineCount, "line")}`,
950
971
  );
951
972
  summary += theme.fg(
952
973
  "warning",
@@ -966,7 +987,7 @@ function formatSearchSummary(
966
987
  const count = countNonEmptyLines(lines);
967
988
  let summary = theme.fg(
968
989
  "muted",
969
- `↳ ${count} ${pluralize(count, unitLabel, pluralLabel)} returned`,
990
+ `${count} ${pluralize(count, unitLabel, pluralLabel)} returned`,
970
991
  );
971
992
  summary += theme.fg(
972
993
  "warning",
@@ -984,7 +1005,7 @@ function formatBashSummary(
984
1005
  const lineCount = lines.length;
985
1006
  const summary = theme.fg(
986
1007
  "muted",
987
- `↳ ${lineCount} ${pluralize(lineCount, "line")} returned`,
1008
+ `${lineCount} ${pluralize(lineCount, "line")} returned`,
988
1009
  );
989
1010
  return summary;
990
1011
  }
@@ -1093,7 +1114,7 @@ function renderBashErrorResult(
1093
1114
  details: BashToolDetails | undefined,
1094
1115
  ): Text {
1095
1116
  const lines = prepareOutputLines(rawOutput, options);
1096
- let text = theme.fg("error", "↳ command failed");
1117
+ let text = theme.fg("error", "command failed");
1097
1118
 
1098
1119
  if (lines.length > 0) {
1099
1120
  const maxLines = getBashPreviewLineLimit(lines, options, config);
@@ -1258,7 +1279,7 @@ function renderMcpResult(
1258
1279
  const lineCount = countNonEmptyLines(lines);
1259
1280
  let summary = theme.fg(
1260
1281
  "muted",
1261
- `↳ ${lineCount} ${pluralize(lineCount, "line")} returned`,
1282
+ `${lineCount} ${pluralize(lineCount, "line")} returned`,
1262
1283
  );
1263
1284
  summary += formatExpandHint(theme);
1264
1285
  if (config.showTruncationHints && truncation.truncated) {
@@ -1613,6 +1634,7 @@ function installToolDisplayApi(getConfig: ConfigGetter): ToolDisplayApi {
1613
1634
  registerResultRenderMiddleware(registration: ToolResultRenderMiddlewareRegistration): string {
1614
1635
  const id = registration.id || `result-middleware-${++nextResultMiddlewareId}`;
1615
1636
  resultRenderMiddlewares.set(id, { ...registration, id });
1637
+ resultRenderMiddlewareRegistrationListener?.();
1616
1638
  return id;
1617
1639
  },
1618
1640
  unregisterResultRenderMiddleware(id: string): boolean {
@@ -1621,6 +1643,9 @@ function installToolDisplayApi(getConfig: ConfigGetter): ToolDisplayApi {
1621
1643
  hasResultRenderMiddleware(id: string): boolean {
1622
1644
  return resultRenderMiddlewares.has(id);
1623
1645
  },
1646
+ listResultRenderMiddlewareToolNames(): string[] {
1647
+ return [...new Set([...resultRenderMiddlewares.values()].map((registration) => registration.toolName))];
1648
+ },
1624
1649
  activateResultRenderPipeline(toolName: string): void {
1625
1650
  activeResultRenderPipelines.add(toolName);
1626
1651
  },
@@ -2045,7 +2070,7 @@ export function registerToolDisplayOverrides(
2045
2070
  }
2046
2071
 
2047
2072
  if (!options.expanded && config.bashCollapsedLines === 0) {
2048
- let hidden = theme.fg("muted", "↳ output hidden");
2073
+ let hidden = theme.fg("muted", "output hidden");
2049
2074
  if (config.showTruncationHints) {
2050
2075
  hidden += formatBashTruncationHints(details, theme);
2051
2076
  }
@@ -2196,6 +2221,118 @@ export function registerToolDisplayOverrides(
2196
2221
  wrappedMcpToolNames.add(toolName);
2197
2222
  };
2198
2223
 
2224
+ /**
2225
+ * 工具结果渲染函数的签名;包装与基线都用它。
2226
+ */
2227
+ type ResultRenderer = (
2228
+ result: unknown,
2229
+ options: ToolRenderResultOptions,
2230
+ theme: RenderTheme,
2231
+ context?: ToolRenderContextLike,
2232
+ ) => unknown;
2233
+
2234
+ /** 已经补过接线的工具名,避免重复包装。 */
2235
+ const resultMiddlewareCoveredToolNames = new Set<string>();
2236
+ /**
2237
+ * 补接线之前的 renderResult(包一层对象,用来区分「本来就没有」和「还没记录」)。
2238
+ *
2239
+ * 宿主被替换而旧宿主没跑清理时,工具上留的是上一轮的包装函数;只记最初那一份,
2240
+ * 重复接线才不会把中间件套成多层。
2241
+ */
2242
+ const originalResultRenderers = new WeakMap<RuntimeToolDefinition, { renderResult?: ResultRenderer }>();
2243
+
2244
+ /** 取工具最初的 renderResult;同一个工具只取一次。 */
2245
+ const getOriginalResultRenderer = (tool: RuntimeToolDefinition): ResultRenderer | undefined => {
2246
+ const cached = originalResultRenderers.get(tool);
2247
+ if (cached) {
2248
+ return cached.renderResult;
2249
+ }
2250
+ const renderResult = typeof tool.renderResult === "function"
2251
+ ? tool.renderResult as ResultRenderer
2252
+ : undefined;
2253
+ originalResultRenderers.set(tool, { renderResult });
2254
+ return renderResult;
2255
+ };
2256
+
2257
+ /** 已注册的中间件是否要求接管这个工具的结果渲染;通配符 `*` 要求所有工具。 */
2258
+ const isResultMiddlewareTarget = (toolName: string): boolean => {
2259
+ const registered = toolDisplayApi.listResultRenderMiddlewareToolNames();
2260
+ return registered.includes("*") || registered.includes(toolName);
2261
+ };
2262
+
2263
+ /**
2264
+ * 是否是 Pi 自己的内建工具。
2265
+ *
2266
+ * 内建工具不在本次覆盖范围:它们的展示由 registerToolOverrides 的开关决定,只在
2267
+ * 这里改渲染函数未必能作用到 Pi 已经建好的工具定义上。
2268
+ */
2269
+ const isPiBuiltInTool = (tool: RuntimeToolDefinition): boolean => {
2270
+ return getTextField(toRecord(tool.sourceInfo), "source") === "builtin";
2271
+ };
2272
+
2273
+ /**
2274
+ * 给单个工具补接线:把它的结果渲染串进中间件链路。
2275
+ *
2276
+ * 只处理我们没接管的工具——内建工具与配置里的自定义工具,各自的渲染函数已经调用过
2277
+ * 中间件。基线优先用工具自带的 renderResult,没有则复刻 Pi 的默认结果块,接线前后
2278
+ * 观感一致,中间件只是多挂一个面板。
2279
+ */
2280
+ const coverToolForResultMiddlewares = (candidate: unknown): boolean => {
2281
+ const tool = candidate as RuntimeToolDefinition;
2282
+ const toolName = getTextField(tool, "name");
2283
+ if (!toolName || isBuiltInToolName(toolName) || isPiBuiltInTool(tool)) {
2284
+ return false;
2285
+ }
2286
+ if (resultMiddlewareCoveredToolNames.has(toolName) || !isResultMiddlewareTarget(toolName)) {
2287
+ return false;
2288
+ }
2289
+
2290
+ const originalRenderResult = getOriginalResultRenderer(tool);
2291
+ const decorated = applyToolDisplayDecorationInPlace(tool, toolDisplayApi, {
2292
+ kind: "generic",
2293
+ overrideExistingRenderers: true,
2294
+ renderResult: (result, options, theme, context) => toolDisplayApi.renderResultWithMiddleware(
2295
+ { toolName, result, options, theme, renderContext: context },
2296
+ () => originalRenderResult
2297
+ ? originalRenderResult(result, options, theme, context)
2298
+ : renderGenericResultPreview(result, options, theme),
2299
+ ),
2300
+ });
2301
+ if (!decorated) {
2302
+ return false;
2303
+ }
2304
+
2305
+ resultMiddlewareCoveredToolNames.add(toolName);
2306
+ toolDisplayApi.activateResultRenderPipeline(toolName);
2307
+ return true;
2308
+ };
2309
+
2310
+ /** 扫描当前全部工具,把被中间件命中的工具接上线。 */
2311
+ const coverToolsForResultMiddlewares = (): void => {
2312
+ const allTools = tryGetAllTools(pi, "Result middleware tool coverage discovery failed.");
2313
+ if (!allTools) {
2314
+ return;
2315
+ }
2316
+ for (const candidate of allTools) {
2317
+ coverToolForResultMiddlewares(candidate);
2318
+ }
2319
+ };
2320
+
2321
+ /** 中间件注册后的回调:注册表变了就重扫一次,把新命中的工具接上线。 */
2322
+ const resultMiddlewareListener = (): void => {
2323
+ coverToolsForResultMiddlewares();
2324
+ };
2325
+ resultRenderMiddlewareRegistrationListener = resultMiddlewareListener;
2326
+ registerCleanup(() => {
2327
+ if (resultRenderMiddlewareRegistrationListener === resultMiddlewareListener) {
2328
+ resultRenderMiddlewareRegistrationListener = undefined;
2329
+ }
2330
+ resultMiddlewareCoveredToolNames.clear();
2331
+ });
2332
+ // 中间件可能早于宿主注册(只进了等待队列),装完立即补一次;工具列表还拿不到时
2333
+ // 交给 session_start / before_agent_start 那次扫描。
2334
+ coverToolsForResultMiddlewares();
2335
+
2199
2336
  const installMcpRegistrationInterceptor = (): void => {
2200
2337
  const piWithInterception = pi as PiWithRegisterToolInterception;
2201
2338
  const existingInterception = piWithInterception[TOOL_DISPLAY_REGISTER_TOOL_INTERCEPTOR_KEY];
@@ -2214,6 +2351,7 @@ export function registerToolDisplayOverrides(
2214
2351
  if (!decorateCustomToolOverrideCandidate(tool)) {
2215
2352
  decorateMcpToolCandidate(tool);
2216
2353
  }
2354
+ coverToolForResultMiddlewares(tool);
2217
2355
  } catch (error) {
2218
2356
  logToolDisplayDebug("Tool display registration decoration failed.", error);
2219
2357
  }
@@ -2248,6 +2386,8 @@ export function registerToolDisplayOverrides(
2248
2386
  if (!decorateCustomToolOverrideCandidate(candidate)) {
2249
2387
  decorateMcpToolCandidate(candidate);
2250
2388
  }
2389
+ // 自定义/MCP 覆盖会整体替换 renderResult,所以补接线必须排在它们之后。
2390
+ coverToolForResultMiddlewares(candidate);
2251
2391
  }
2252
2392
  };
2253
2393