pi-extensions-tool-display 1.2.0 → 1.3.1

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.2.0",
3
+ "version": "1.3.1",
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.5.0"
51
+ "pi-extensions-i18n": "^0.7.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@earendil-works/pi-coding-agent": ">=0.80.0",
package/src/index.ts CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  type ToolDisplayConfig,
24
24
  } from "./types.js";
25
25
  import {
26
+ installNoticeRenderer,
26
27
  notifyWithSource,
27
28
  type NoticeColor,
28
29
  type NoticeSource,
@@ -180,5 +181,7 @@ export default function toolDisplayExtension(
180
181
  pi: ExtensionAPI,
181
182
  initial: ConfigLoadResult = loadToolDisplayConfig(),
182
183
  ): void {
184
+ // 提示画成会话区里的带底色消息块;渲染器在本包这个模块实例里注册一次。
185
+ installNoticeRenderer(pi);
183
186
  ensureToolDisplayHost(pi, initial);
184
187
  }
@@ -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
+ }
@@ -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,
@@ -183,6 +184,8 @@ export interface ToolDisplayApi {
183
184
  registerResultRenderMiddleware(registration: ToolResultRenderMiddlewareRegistration): string;
184
185
  unregisterResultRenderMiddleware(id: string): boolean;
185
186
  hasResultRenderMiddleware(id: string): boolean;
187
+ /** 已注册中间件命中过的工具名,含通配符 `*`。 */
188
+ listResultRenderMiddlewareToolNames(): string[];
186
189
  activateResultRenderPipeline(toolName: string): void;
187
190
  isResultRenderPipelineActive(toolName: string): boolean;
188
191
  renderResultWithMiddleware(
@@ -238,6 +241,15 @@ type PiWithRegisterToolInterception = ExtensionAPI & {
238
241
  const decoratedToolDescriptors = new WeakMap<RuntimeToolDefinition, ToolPropertyDescriptorSnapshot>();
239
242
  const decoratedTools = new Set<RuntimeToolDefinition>();
240
243
 
244
+ /**
245
+ * 中间件注册之后的回调。
246
+ *
247
+ * 给工具补接线需要 pi 与当前工具列表,只有宿主拿得到,因此宿主在
248
+ * registerToolDisplayOverrides 里把自己那份实现挂上来;中间件在宿主安装之后才注册时
249
+ * (扩展加载顺序不固定),靠它立刻补上接线。
250
+ */
251
+ let resultRenderMiddlewareRegistrationListener: (() => void) | undefined;
252
+
241
253
  function registerRuntimeTool(pi: ExtensionAPI, tool: RuntimeToolDefinition): void {
242
254
  pi.registerTool(tool as unknown as ToolDefinition);
243
255
  }
@@ -1613,6 +1625,7 @@ function installToolDisplayApi(getConfig: ConfigGetter): ToolDisplayApi {
1613
1625
  registerResultRenderMiddleware(registration: ToolResultRenderMiddlewareRegistration): string {
1614
1626
  const id = registration.id || `result-middleware-${++nextResultMiddlewareId}`;
1615
1627
  resultRenderMiddlewares.set(id, { ...registration, id });
1628
+ resultRenderMiddlewareRegistrationListener?.();
1616
1629
  return id;
1617
1630
  },
1618
1631
  unregisterResultRenderMiddleware(id: string): boolean {
@@ -1621,6 +1634,9 @@ function installToolDisplayApi(getConfig: ConfigGetter): ToolDisplayApi {
1621
1634
  hasResultRenderMiddleware(id: string): boolean {
1622
1635
  return resultRenderMiddlewares.has(id);
1623
1636
  },
1637
+ listResultRenderMiddlewareToolNames(): string[] {
1638
+ return [...new Set([...resultRenderMiddlewares.values()].map((registration) => registration.toolName))];
1639
+ },
1624
1640
  activateResultRenderPipeline(toolName: string): void {
1625
1641
  activeResultRenderPipelines.add(toolName);
1626
1642
  },
@@ -2196,6 +2212,118 @@ export function registerToolDisplayOverrides(
2196
2212
  wrappedMcpToolNames.add(toolName);
2197
2213
  };
2198
2214
 
2215
+ /**
2216
+ * 工具结果渲染函数的签名;包装与基线都用它。
2217
+ */
2218
+ type ResultRenderer = (
2219
+ result: unknown,
2220
+ options: ToolRenderResultOptions,
2221
+ theme: RenderTheme,
2222
+ context?: ToolRenderContextLike,
2223
+ ) => unknown;
2224
+
2225
+ /** 已经补过接线的工具名,避免重复包装。 */
2226
+ const resultMiddlewareCoveredToolNames = new Set<string>();
2227
+ /**
2228
+ * 补接线之前的 renderResult(包一层对象,用来区分「本来就没有」和「还没记录」)。
2229
+ *
2230
+ * 宿主被替换而旧宿主没跑清理时,工具上留的是上一轮的包装函数;只记最初那一份,
2231
+ * 重复接线才不会把中间件套成多层。
2232
+ */
2233
+ const originalResultRenderers = new WeakMap<RuntimeToolDefinition, { renderResult?: ResultRenderer }>();
2234
+
2235
+ /** 取工具最初的 renderResult;同一个工具只取一次。 */
2236
+ const getOriginalResultRenderer = (tool: RuntimeToolDefinition): ResultRenderer | undefined => {
2237
+ const cached = originalResultRenderers.get(tool);
2238
+ if (cached) {
2239
+ return cached.renderResult;
2240
+ }
2241
+ const renderResult = typeof tool.renderResult === "function"
2242
+ ? tool.renderResult as ResultRenderer
2243
+ : undefined;
2244
+ originalResultRenderers.set(tool, { renderResult });
2245
+ return renderResult;
2246
+ };
2247
+
2248
+ /** 已注册的中间件是否要求接管这个工具的结果渲染;通配符 `*` 要求所有工具。 */
2249
+ const isResultMiddlewareTarget = (toolName: string): boolean => {
2250
+ const registered = toolDisplayApi.listResultRenderMiddlewareToolNames();
2251
+ return registered.includes("*") || registered.includes(toolName);
2252
+ };
2253
+
2254
+ /**
2255
+ * 是否是 Pi 自己的内建工具。
2256
+ *
2257
+ * 内建工具不在本次覆盖范围:它们的展示由 registerToolOverrides 的开关决定,只在
2258
+ * 这里改渲染函数未必能作用到 Pi 已经建好的工具定义上。
2259
+ */
2260
+ const isPiBuiltInTool = (tool: RuntimeToolDefinition): boolean => {
2261
+ return getTextField(toRecord(tool.sourceInfo), "source") === "builtin";
2262
+ };
2263
+
2264
+ /**
2265
+ * 给单个工具补接线:把它的结果渲染串进中间件链路。
2266
+ *
2267
+ * 只处理我们没接管的工具——内建工具与配置里的自定义工具,各自的渲染函数已经调用过
2268
+ * 中间件。基线优先用工具自带的 renderResult,没有则复刻 Pi 的默认结果块,接线前后
2269
+ * 观感一致,中间件只是多挂一个面板。
2270
+ */
2271
+ const coverToolForResultMiddlewares = (candidate: unknown): boolean => {
2272
+ const tool = candidate as RuntimeToolDefinition;
2273
+ const toolName = getTextField(tool, "name");
2274
+ if (!toolName || isBuiltInToolName(toolName) || isPiBuiltInTool(tool)) {
2275
+ return false;
2276
+ }
2277
+ if (resultMiddlewareCoveredToolNames.has(toolName) || !isResultMiddlewareTarget(toolName)) {
2278
+ return false;
2279
+ }
2280
+
2281
+ const originalRenderResult = getOriginalResultRenderer(tool);
2282
+ const decorated = applyToolDisplayDecorationInPlace(tool, toolDisplayApi, {
2283
+ kind: "generic",
2284
+ overrideExistingRenderers: true,
2285
+ renderResult: (result, options, theme, context) => toolDisplayApi.renderResultWithMiddleware(
2286
+ { toolName, result, options, theme, renderContext: context },
2287
+ () => originalRenderResult
2288
+ ? originalRenderResult(result, options, theme, context)
2289
+ : renderGenericResultPreview(result, options, theme),
2290
+ ),
2291
+ });
2292
+ if (!decorated) {
2293
+ return false;
2294
+ }
2295
+
2296
+ resultMiddlewareCoveredToolNames.add(toolName);
2297
+ toolDisplayApi.activateResultRenderPipeline(toolName);
2298
+ return true;
2299
+ };
2300
+
2301
+ /** 扫描当前全部工具,把被中间件命中的工具接上线。 */
2302
+ const coverToolsForResultMiddlewares = (): void => {
2303
+ const allTools = tryGetAllTools(pi, "Result middleware tool coverage discovery failed.");
2304
+ if (!allTools) {
2305
+ return;
2306
+ }
2307
+ for (const candidate of allTools) {
2308
+ coverToolForResultMiddlewares(candidate);
2309
+ }
2310
+ };
2311
+
2312
+ /** 中间件注册后的回调:注册表变了就重扫一次,把新命中的工具接上线。 */
2313
+ const resultMiddlewareListener = (): void => {
2314
+ coverToolsForResultMiddlewares();
2315
+ };
2316
+ resultRenderMiddlewareRegistrationListener = resultMiddlewareListener;
2317
+ registerCleanup(() => {
2318
+ if (resultRenderMiddlewareRegistrationListener === resultMiddlewareListener) {
2319
+ resultRenderMiddlewareRegistrationListener = undefined;
2320
+ }
2321
+ resultMiddlewareCoveredToolNames.clear();
2322
+ });
2323
+ // 中间件可能早于宿主注册(只进了等待队列),装完立即补一次;工具列表还拿不到时
2324
+ // 交给 session_start / before_agent_start 那次扫描。
2325
+ coverToolsForResultMiddlewares();
2326
+
2199
2327
  const installMcpRegistrationInterceptor = (): void => {
2200
2328
  const piWithInterception = pi as PiWithRegisterToolInterception;
2201
2329
  const existingInterception = piWithInterception[TOOL_DISPLAY_REGISTER_TOOL_INTERCEPTOR_KEY];
@@ -2214,6 +2342,7 @@ export function registerToolDisplayOverrides(
2214
2342
  if (!decorateCustomToolOverrideCandidate(tool)) {
2215
2343
  decorateMcpToolCandidate(tool);
2216
2344
  }
2345
+ coverToolForResultMiddlewares(tool);
2217
2346
  } catch (error) {
2218
2347
  logToolDisplayDebug("Tool display registration decoration failed.", error);
2219
2348
  }
@@ -2248,6 +2377,8 @@ export function registerToolDisplayOverrides(
2248
2377
  if (!decorateCustomToolOverrideCandidate(candidate)) {
2249
2378
  decorateMcpToolCandidate(candidate);
2250
2379
  }
2380
+ // 自定义/MCP 覆盖会整体替换 renderResult,所以补接线必须排在它们之后。
2381
+ coverToolForResultMiddlewares(candidate);
2251
2382
  }
2252
2383
  };
2253
2384