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 +2 -0
- package/README.zh-CN.md +2 -0
- package/package.json +2 -2
- package/src/config-modal.ts +3 -2
- package/src/diff-presentation.ts +3 -3
- package/src/diff-renderer.ts +9 -9
- package/src/index.ts +3 -2
- package/src/result-middleware-coverage.ts +51 -0
- package/src/thinking-label.ts +3 -2
- package/src/tool-overrides.ts +149 -9
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.
|
|
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.
|
|
51
|
+
"pi-extensions-i18n": "^0.8.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"@earendil-works/pi-coding-agent": ">=0.80.0",
|
package/src/config-modal.ts
CHANGED
|
@@ -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 =
|
|
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
|
|
package/src/diff-presentation.ts
CHANGED
|
@@ -54,9 +54,9 @@ export function buildDiffSummaryText(stats: DiffSummaryStats, width: number): st
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
const summaryCandidates = [
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
"…",
|
package/src/diff-renderer.ts
CHANGED
|
@@ -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",
|
|
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", "
|
|
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",
|
|
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", "
|
|
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",
|
|
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
|
-
|
|
2372
|
-
|
|
2373
|
-
"
|
|
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", "
|
|
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 =
|
|
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
|
+
}
|
package/src/thinking-label.ts
CHANGED
|
@@ -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 =
|
|
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
|
|
package/src/tool-overrides.ts
CHANGED
|
@@ -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", "
|
|
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", "
|
|
485
|
+
return theme.fg("muted", "command completed (no output)");
|
|
465
486
|
}
|
|
466
|
-
return theme.fg("muted", "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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", "
|
|
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
|
-
|
|
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", "
|
|
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
|
|