@herbertgao/pi-extensions 2026.9.19 → 2026.9.20

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.
Files changed (186) hide show
  1. package/README.md +1 -8
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +76 -59
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +76 -59
  4. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/preview.webp +0 -0
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +93 -34
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +346 -117
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/core.ts +6 -36
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary/index.ts +2 -2
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +24 -29
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +330 -360
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +56 -108
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/session.ts +97 -15
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/subagent.ts +80 -45
  14. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/flush-docked-bash.ts +36 -0
  15. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer-layout.ts +229 -0
  16. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/footer.ts +479 -0
  17. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/startup-header.ts +92 -61
  18. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/shell/working-message.ts +58 -38
  19. package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +19 -3
  20. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +1080 -229
  21. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +232 -259
  22. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +38 -48
  23. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +8 -2
  24. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +2 -7
  25. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +277 -113
  26. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +15 -6
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +18 -72
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +61 -26
  29. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/ansi-utils.ts +25 -57
  30. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-component.ts +125 -0
  31. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-edit-render.ts +227 -0
  32. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-header.ts +136 -0
  33. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-highlight.ts +115 -0
  34. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-inline.ts +324 -0
  35. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-layout.ts +762 -0
  36. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-limits.ts +152 -0
  37. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-palette.ts +410 -0
  38. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-parse.ts +535 -0
  39. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-presentation.ts +2 -2
  40. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +21 -3219
  41. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-text.ts +44 -0
  42. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-write-render.ts +568 -0
  43. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/index.ts +19 -18
  44. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/line-width-safety.ts +2 -2
  45. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/write-execution.ts +1 -0
  46. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +142 -192
  47. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/mcp-title.ts +39 -0
  48. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{message-display.ts → tool/message-display.ts} +24 -90
  49. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/names.ts +426 -0
  50. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +90 -132
  51. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/{show-more-hint.ts → tool/show-more-hint.ts} +10 -0
  52. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/transcript-refresh.ts +8 -26
  53. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/ansi-color.ts +44 -0
  54. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/ansi-text.ts +76 -0
  55. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/component-tree.ts +34 -0
  56. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/format.ts +28 -0
  57. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/fullscreen-detect.ts +51 -0
  58. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/patch-keys.ts +124 -0
  59. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/sgr-mouse.ts +70 -0
  60. package/node_modules/@herbertgao/pi-cc-extensions/package.json +7 -7
  61. package/package.json +3 -7
  62. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/hero.en.svg +0 -101
  63. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/hero.svg +0 -101
  64. package/node_modules/@herbertgao/pi-cc-extensions/assets/readme/welcome.webp +0 -0
  65. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/render-utils.ts +0 -5
  66. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/types.ts +0 -30
  67. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/write-display-utils.ts +0 -12
  68. package/node_modules/pi-footer/CHANGELOG.md +0 -145
  69. package/node_modules/pi-footer/LICENSE +0 -21
  70. package/node_modules/pi-footer/README.md +0 -354
  71. package/node_modules/pi-footer/package.json +0 -68
  72. package/node_modules/pi-footer/src/cache.ts +0 -106
  73. package/node_modules/pi-footer/src/colors.ts +0 -237
  74. package/node_modules/pi-footer/src/config.ts +0 -202
  75. package/node_modules/pi-footer/src/event-widgets.ts +0 -41
  76. package/node_modules/pi-footer/src/extension-statuses.ts +0 -102
  77. package/node_modules/pi-footer/src/git.ts +0 -135
  78. package/node_modules/pi-footer/src/index.ts +0 -268
  79. package/node_modules/pi-footer/src/metrics.ts +0 -141
  80. package/node_modules/pi-footer/src/presets.ts +0 -383
  81. package/node_modules/pi-footer/src/render.ts +0 -114
  82. package/node_modules/pi-footer/src/separators.ts +0 -52
  83. package/node_modules/pi-footer/src/types.ts +0 -122
  84. package/node_modules/pi-footer/src/ui/color-level-confirm.ts +0 -22
  85. package/node_modules/pi-footer/src/ui/color-options.ts +0 -195
  86. package/node_modules/pi-footer/src/ui/config-lifecycle.ts +0 -61
  87. package/node_modules/pi-footer/src/ui/edit-colors.ts +0 -13
  88. package/node_modules/pi-footer/src/ui/events.ts +0 -35
  89. package/node_modules/pi-footer/src/ui/extension-status-picker.ts +0 -60
  90. package/node_modules/pi-footer/src/ui/extension-statuses.ts +0 -73
  91. package/node_modules/pi-footer/src/ui/fields.ts +0 -252
  92. package/node_modules/pi-footer/src/ui/global-menu.ts +0 -126
  93. package/node_modules/pi-footer/src/ui/helpers.ts +0 -44
  94. package/node_modules/pi-footer/src/ui/layout.ts +0 -17
  95. package/node_modules/pi-footer/src/ui/line-list.ts +0 -19
  96. package/node_modules/pi-footer/src/ui/model.ts +0 -64
  97. package/node_modules/pi-footer/src/ui/navigation.ts +0 -29
  98. package/node_modules/pi-footer/src/ui/option-edit.ts +0 -93
  99. package/node_modules/pi-footer/src/ui/overlay-render.ts +0 -103
  100. package/node_modules/pi-footer/src/ui/screen-context.ts +0 -18
  101. package/node_modules/pi-footer/src/ui/screen-controller.ts +0 -27
  102. package/node_modules/pi-footer/src/ui/screen-render.ts +0 -32
  103. package/node_modules/pi-footer/src/ui/screen-state.ts +0 -20
  104. package/node_modules/pi-footer/src/ui/screen.ts +0 -243
  105. package/node_modules/pi-footer/src/ui/screens/add-widget.ts +0 -92
  106. package/node_modules/pi-footer/src/ui/screens/confirm-exit.ts +0 -52
  107. package/node_modules/pi-footer/src/ui/screens/controller.ts +0 -12
  108. package/node_modules/pi-footer/src/ui/screens/edit-colors.ts +0 -68
  109. package/node_modules/pi-footer/src/ui/screens/edit-widget.ts +0 -115
  110. package/node_modules/pi-footer/src/ui/screens/extension-status-row.ts +0 -61
  111. package/node_modules/pi-footer/src/ui/screens/global.ts +0 -75
  112. package/node_modules/pi-footer/src/ui/screens/line-list.ts +0 -108
  113. package/node_modules/pi-footer/src/ui/screens/main.ts +0 -99
  114. package/node_modules/pi-footer/src/ui/screens/terminal.ts +0 -121
  115. package/node_modules/pi-footer/src/ui/screens/widget-list.ts +0 -154
  116. package/node_modules/pi-footer/src/ui/terminal-menu.ts +0 -32
  117. package/node_modules/pi-footer/src/ui/theme.ts +0 -48
  118. package/node_modules/pi-footer/src/ui/title-bar.ts +0 -100
  119. package/node_modules/pi-footer/src/ui/widget-actions.ts +0 -76
  120. package/node_modules/pi-footer/src/ui.ts +0 -66
  121. package/node_modules/pi-footer/src/widgets/context.ts +0 -28
  122. package/node_modules/pi-footer/src/widgets/core/active-tools.ts +0 -17
  123. package/node_modules/pi-footer/src/widgets/core/context-window.ts +0 -24
  124. package/node_modules/pi-footer/src/widgets/core/cwd-basename.ts +0 -19
  125. package/node_modules/pi-footer/src/widgets/core/cwd.ts +0 -108
  126. package/node_modules/pi-footer/src/widgets/core/event.ts +0 -34
  127. package/node_modules/pi-footer/src/widgets/core/external-status.ts +0 -102
  128. package/node_modules/pi-footer/src/widgets/core/model-provider.ts +0 -20
  129. package/node_modules/pi-footer/src/widgets/core/model.ts +0 -35
  130. package/node_modules/pi-footer/src/widgets/core/provider.ts +0 -17
  131. package/node_modules/pi-footer/src/widgets/core/session-name.ts +0 -17
  132. package/node_modules/pi-footer/src/widgets/core/text-verbosity.ts +0 -17
  133. package/node_modules/pi-footer/src/widgets/core/thinking-level.ts +0 -17
  134. package/node_modules/pi-footer/src/widgets/git/ahead-behind.ts +0 -17
  135. package/node_modules/pi-footer/src/widgets/git/branch.ts +0 -81
  136. package/node_modules/pi-footer/src/widgets/git/clean.ts +0 -19
  137. package/node_modules/pi-footer/src/widgets/git/deletions.ts +0 -17
  138. package/node_modules/pi-footer/src/widgets/git/diff.ts +0 -38
  139. package/node_modules/pi-footer/src/widgets/git/insertions.ts +0 -17
  140. package/node_modules/pi-footer/src/widgets/git/remote.ts +0 -17
  141. package/node_modules/pi-footer/src/widgets/git/root.ts +0 -17
  142. package/node_modules/pi-footer/src/widgets/git/sha.ts +0 -17
  143. package/node_modules/pi-footer/src/widgets/git/staged.ts +0 -17
  144. package/node_modules/pi-footer/src/widgets/git/status.ts +0 -19
  145. package/node_modules/pi-footer/src/widgets/git/unstaged.ts +0 -17
  146. package/node_modules/pi-footer/src/widgets/git/untracked.ts +0 -17
  147. package/node_modules/pi-footer/src/widgets/instance.ts +0 -118
  148. package/node_modules/pi-footer/src/widgets/layout/custom-text.ts +0 -18
  149. package/node_modules/pi-footer/src/widgets/layout/flex-separator.ts +0 -32
  150. package/node_modules/pi-footer/src/widgets/layout/separator.ts +0 -57
  151. package/node_modules/pi-footer/src/widgets/layout/spacer.ts +0 -31
  152. package/node_modules/pi-footer/src/widgets/options.ts +0 -122
  153. package/node_modules/pi-footer/src/widgets/project/runtime.ts +0 -361
  154. package/node_modules/pi-footer/src/widgets/registry.ts +0 -223
  155. package/node_modules/pi-footer/src/widgets/session/assistant-messages.ts +0 -17
  156. package/node_modules/pi-footer/src/widgets/session/compactions.ts +0 -17
  157. package/node_modules/pi-footer/src/widgets/session/elapsed.ts +0 -18
  158. package/node_modules/pi-footer/src/widgets/session/last-activity.ts +0 -18
  159. package/node_modules/pi-footer/src/widgets/session/messages.ts +0 -19
  160. package/node_modules/pi-footer/src/widgets/session/session-id.ts +0 -17
  161. package/node_modules/pi-footer/src/widgets/session/session-start.ts +0 -18
  162. package/node_modules/pi-footer/src/widgets/session/tool-results.ts +0 -17
  163. package/node_modules/pi-footer/src/widgets/session/total-messages.ts +0 -19
  164. package/node_modules/pi-footer/src/widgets/session/total-time.ts +0 -18
  165. package/node_modules/pi-footer/src/widgets/session/user-messages.ts +0 -17
  166. package/node_modules/pi-footer/src/widgets/store.ts +0 -26
  167. package/node_modules/pi-footer/src/widgets/tokens/cache-hit-rate.ts +0 -55
  168. package/node_modules/pi-footer/src/widgets/tokens/cache-read.ts +0 -18
  169. package/node_modules/pi-footer/src/widgets/tokens/cache-write.ts +0 -18
  170. package/node_modules/pi-footer/src/widgets/tokens/context-bar.ts +0 -108
  171. package/node_modules/pi-footer/src/widgets/tokens/context-length.ts +0 -26
  172. package/node_modules/pi-footer/src/widgets/tokens/context-remaining.ts +0 -29
  173. package/node_modules/pi-footer/src/widgets/tokens/context.ts +0 -29
  174. package/node_modules/pi-footer/src/widgets/tokens/cost.ts +0 -51
  175. package/node_modules/pi-footer/src/widgets/tokens/input-speed.ts +0 -25
  176. package/node_modules/pi-footer/src/widgets/tokens/input-tokens.ts +0 -18
  177. package/node_modules/pi-footer/src/widgets/tokens/output-speed.ts +0 -25
  178. package/node_modules/pi-footer/src/widgets/tokens/output-tokens.ts +0 -18
  179. package/node_modules/pi-footer/src/widgets/tokens/tokens.ts +0 -20
  180. package/node_modules/pi-footer/src/widgets/tokens/total-speed.ts +0 -25
  181. package/node_modules/pi-footer/src/widgets/tokens/total-tokens.ts +0 -18
  182. package/node_modules/pi-footer/src/widgets/types.ts +0 -181
  183. package/node_modules/pi-footer/src/widgets/utils/colors.ts +0 -11
  184. package/node_modules/pi-footer/src/widgets/utils/context.ts +0 -112
  185. package/node_modules/pi-footer/src/widgets/utils/session.ts +0 -19
  186. package/node_modules/pi-footer/src/widgets/utils/token-format.ts +0 -78
@@ -7,16 +7,19 @@
7
7
  import { ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
8
8
  import { Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
9
9
  import { config, getToolDisplayConfig, type CompactStyleMode } from "../config/config.ts";
10
- import { isToolCallHovered } from "./mouse/interaction.ts";
10
+ import {
11
+ COMPONENT_TOOL_RENDER_MODE,
12
+ GLOBAL_TOOL_RENDER_PATCH,
13
+ patchRegistry,
14
+ TOOL_EXPANDED_BACKGROUND_PATCH,
15
+ } from "../utils/patch-keys.ts";
16
+ import { isToolCallHovered } from "./mouse/hover.ts";
11
17
  import {
12
18
  countLines,
13
- fitToolCallSummary,
14
19
  hasExpandableDetail,
15
20
  insetComponent,
16
21
  isToolExpanded,
17
- oneLine,
18
22
  outputLineCount,
19
- pathSummary,
20
23
  pendingIcon,
21
24
  renderCollapsedToolResultToWidth,
22
25
  renderExpandedToolResult,
@@ -27,27 +30,24 @@ import {
27
30
  textFromResult,
28
31
  toolIconColor,
29
32
  toolViewportWidth,
30
- type ToolCallSummary,
31
33
  } from "./tool/result.ts";
32
- import { showMoreHintText } from "./show-more-hint.ts";
34
+ import { oneLine } from "../utils/format.ts";
35
+ import { showMoreHintText } from "./tool/show-more-hint.ts";
36
+ import { countWriteDiffStats } from "./tool/diff/diff-renderer.ts";
33
37
  import { renderRichToolResult, type WriteExecutionMetadataStore } from "./tool/diff/index.ts";
34
- import { getMessageDisplayTheme } from "./message-display.ts";
38
+ import { getMessageDisplayTheme } from "./tool/message-display.ts";
39
+ import { mcpToolTitle } from "./tool/mcp-title.ts";
40
+ import {
41
+ fitToolCallSummary,
42
+ humanizeToolLabel,
43
+ renderToolSummary,
44
+ toolCallSummary,
45
+ } from "./tool/names.ts";
35
46
 
36
47
  // 成功勾:亮绿 truecolor(与 message-display 一致)
37
48
  const BRIGHT_GREEN = "\x1b[38;2;80;220;100m";
38
49
  const ANSI_FG_RESET = "\x1b[39m";
39
50
 
40
- const GLOBAL_TOOL_RENDER_PATCH = Symbol.for("pi.ccstyle.global-tool-render-patch");
41
- const COMPONENT_TOOL_RENDER_MODE = Symbol.for("pi.ccstyle.component-tool-render-mode");
42
- const COMPONENT_TOOL_SELF_SHELL_MODE = Symbol.for("pi.ccstyle.component-tool-self-shell-mode");
43
- const TOOL_EXPANDED_BACKGROUND_PATCH = Symbol.for("pi.ccstyle.tool-expanded-background-patch");
44
-
45
- const AGENT_FAMILY_TOOL_NAMES = new Set([
46
- "Agent",
47
- "Agents",
48
- "get_subagent_result",
49
- "steer_subagent",
50
- ]);
51
51
  // pi-subagents 等扩展为 Agent 提供专用渲染器,ccstyle 必须保留。
52
52
  const DEDICATED_RENDERER_TOOLS = new Set(["Agent"]);
53
53
 
@@ -64,23 +64,32 @@ type ToolRenderMethods = {
64
64
  getResultRenderer: (...args: any[]) => any;
65
65
  };
66
66
 
67
+ type ToolPaintHit = {
68
+ width: number;
69
+ expanded: boolean;
70
+ isPartial: boolean;
71
+ result: unknown;
72
+ args: unknown;
73
+ callHover: boolean;
74
+ ioHover: string | null;
75
+ lines: string[];
76
+ };
77
+
67
78
  type GlobalToolRenderPatch = {
68
79
  version: 2;
69
80
  prototype: any;
70
- owner: object;
71
81
  active: boolean;
72
- enabled: () => boolean;
73
82
  mode: () => CompactStyleMode;
74
83
  wrap: (tool: any) => any;
75
84
  byDefinition: WeakMap<object, any>;
76
85
  byName: Map<string, any>;
77
86
  downstream: ToolRenderMethods;
78
87
  installed: ToolRenderMethods;
79
- // 兼容旧扩展读取 Symbol 上的 legacy 字段。
80
- originalHasRendererDefinition: ToolRenderMethods["hasRendererDefinition"];
81
- originalGetRenderShell: ToolRenderMethods["getRenderShell"];
82
- originalGetCallRenderer: ToolRenderMethods["getCallRenderer"];
83
- originalGetResultRenderer: ToolRenderMethods["getResultRenderer"];
88
+ originalRender?: (width: number) => string[];
89
+ originalInvalidate?: () => void;
90
+ renderPaint?: (width: number) => string[];
91
+ invalidatePaint?: () => void;
92
+ paintCache?: WeakMap<object, ToolPaintHit>;
84
93
  };
85
94
 
86
95
  type ToolExpandedBackgroundPatch = {
@@ -91,6 +100,8 @@ type ToolExpandedBackgroundPatch = {
91
100
  dispose: () => void;
92
101
  };
93
102
 
103
+ /** rich diff 的两个入口(本处与 compact 的 paintCompactEditWrite)都靠 renderRichToolResult
104
+ * 返回 undefined 让位,这里只判断模式与工具名,避免多一个可能与让位不一致的 gate。 */
94
105
  export function shouldRenderRichDiff(
95
106
  mode: CompactStyleMode,
96
107
  toolName: string,
@@ -99,25 +110,6 @@ export function shouldRenderRichDiff(
99
110
  return mode === "on" && !isError && (toolName === "edit" || toolName === "write");
100
111
  }
101
112
 
102
- export function isMcpToolDefinition(definition: any, toolName: string): boolean {
103
- const label = typeof definition?.label === "string" ? definition.label.trim() : "";
104
- if (/^MCP(?::|$)/i.test(label)) return true;
105
- if (toolName === "mcp" || /^mcp[_:-]|[_:-]mcp[_:-]/i.test(toolName)) return true;
106
- if (label) return false;
107
- const description = typeof definition?.description === "string" ? definition.description : "";
108
- return /\bModel Context Protocol\b/i.test(description);
109
- }
110
-
111
- export function humanizeMcpToolName(toolName: string): string {
112
- const words = toolName
113
- .replace(/^mcp(?:[_:-]+)+/i, "")
114
- .split(/[_:-]+/)
115
- .filter(Boolean);
116
- return words.length
117
- ? words.map((word) => word[0]!.toUpperCase() + word.slice(1)).join(" ")
118
- : "MCP";
119
- }
120
-
121
113
  /** 排除名单内且自带 renderer 的工具保留原渲染。 */
122
114
  export function preservesOriginalRenderer(
123
115
  extensionDefinition: any,
@@ -143,97 +135,6 @@ function renderDefault(tool: any, slot: "renderCall" | "renderResult", args: any
143
135
  return new Text(fallback, 0, 0);
144
136
  }
145
137
 
146
- function humanizeToolLabel(label: string): string {
147
- return label
148
- .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
149
- .replace(/[_-]+/g, " ")
150
- .replace(/\b\w/g, (char) => char.toUpperCase());
151
- }
152
-
153
- function singleToolCallSummary(
154
- toolName: string,
155
- label: string,
156
- args: any,
157
- cwd?: string,
158
- ): ToolCallSummary {
159
- const title = label === toolName ? humanizeToolLabel(label) : label;
160
- if (!args || typeof args !== "object") return { main: title, detail: "" };
161
- const name = toolName.toLowerCase();
162
- const value = (fallback: string, ...keys: string[]) => {
163
- const found = keys.map((key) => args[key]).find((item) => typeof item === "string" && item);
164
- return `${title} ${oneLine(found || fallback)}`;
165
- };
166
- if (AGENT_FAMILY_TOOL_NAMES.has(toolName) && args.agent_id) {
167
- return { main: `${title} ${oneLine(args.agent_id)}`, detail: "" };
168
- }
169
- // Agents 走 ccstyle wrapper;Agent 保留专用渲染器。
170
- if (name === "agents") {
171
- return {
172
- main: value("launch agents", "description", "prompt"),
173
- detail: "",
174
- };
175
- }
176
- if (name === "skill") return { main: value("run skill", "name"), detail: "" };
177
- if (name === "enterplanmode" || name === "enter_plan_mode") {
178
- return { main: `${title} enable read-only planning`, detail: "" };
179
- }
180
- if (name === "exitplanmode" || name === "exit_plan_mode") {
181
- return { main: `${title} present plan`, detail: "" };
182
- }
183
- if (name === "taskcreate") return { main: value("create task", "subject"), detail: "" };
184
- if (name === "tasklist") return { main: `${title} task list`, detail: "" };
185
- if (name === "taskget" || name === "taskupdate") {
186
- return { main: value("task", "taskId", "task_id"), detail: "" };
187
- }
188
- if (name === "taskoutput" || name === "taskstop") {
189
- return { main: value("background task", "task_id", "taskId"), detail: "" };
190
- }
191
- if (name === "taskexecute") {
192
- const ids = Array.isArray(args.task_ids)
193
- ? args.task_ids
194
- : Array.isArray(args.taskIds)
195
- ? args.taskIds
196
- : [];
197
- const summary = ids.length
198
- ? `${ids[0]}${ids.length > 1 ? ` (+${ids.length - 1} tasks)` : ""}`
199
- : "start tasks";
200
- return { main: `${title} ${summary}`, detail: "" };
201
- }
202
- if (toolName === "read") {
203
- const details = [
204
- args.offset !== undefined ? `offset=${args.offset}` : "",
205
- args.limit !== undefined ? `limit=${args.limit}` : "",
206
- ].filter(Boolean);
207
- const detail = details.length ? ` (${details.join(", ")})` : "";
208
- if (typeof args.path === "string" && args.path) {
209
- return pathSummary(title, args.path, cwd, detail);
210
- }
211
- return { main: title, detail };
212
- }
213
- const preferredPath = args.path ?? args.file_path;
214
- if (typeof preferredPath === "string" && preferredPath) {
215
- return pathSummary(title, preferredPath, cwd);
216
- }
217
- const preferred =
218
- args.command ??
219
- args.query ??
220
- args.question ??
221
- args.pattern ??
222
- args.url ??
223
- args.name ??
224
- args.tool_use_id ??
225
- args.toolCallId ??
226
- args.id ??
227
- args.message;
228
- return {
229
- main:
230
- preferred !== undefined && preferred !== null && typeof preferred !== "object"
231
- ? `${title} ${oneLine(preferred)}`
232
- : title,
233
- detail: "",
234
- };
235
- }
236
-
237
138
  type ParsedTask = { id: string; status: string; subject: string };
238
139
 
239
140
  function parseTaskList(text: string): ParsedTask[] {
@@ -277,11 +178,16 @@ function renderExpandedTaskResult(
277
178
  : task.status === "in_progress"
278
179
  ? "warning"
279
180
  : "muted";
280
- return ` ${theme.fg("accent", `#${task.id}`)} ${theme.fg(color, task.status)} ${theme.fg("dim", task.subject)}`;
181
+ return `${theme.fg("accent", `#${task.id}`)} ${theme.fg(color, task.status)} ${theme.fg("dim", task.subject)}`;
281
182
  });
282
183
  if (tasks.length > rows.length)
283
- rows.push(theme.fg("muted", ` … ${tasks.length - rows.length} more tasks`));
284
- return new Text(` ↳ ${theme.fg("muted", taskListSummary(tasks))}\n${rows.join("\n")}`, 0, 0);
184
+ rows.push(theme.fg("muted", `… ${tasks.length - rows.length} more tasks`));
185
+ // 贴左:外层展开卡片 Box(1,1) 提供 1 格 padding
186
+ return new Text(
187
+ `↳ ${theme.fg("muted", taskListSummary(tasks))}\n${rows.map((r) => ` ${r}`).join("\n")}`,
188
+ 0,
189
+ 0,
190
+ );
285
191
  }
286
192
  const line = text.trim();
287
193
  if (!line || line.includes("\n")) return undefined;
@@ -299,7 +205,7 @@ function renderExpandedTaskResult(
299
205
  } else if (toolName === "TaskStop") {
300
206
  formatted = `${theme.fg("success", "Stopped")} ${theme.fg("muted", line)}`;
301
207
  }
302
- return formatted ? new Text(` ↳ ${formatted}`, 0, 0) : undefined;
208
+ return formatted ? new Text(`↳ ${formatted}`, 0, 0) : undefined;
303
209
  }
304
210
 
305
211
  /** 用 ccstyle call/result 包装任意工具定义。 */
@@ -308,15 +214,16 @@ function createCcstyleTool(
308
214
  writeExecutionMetadata: WriteExecutionMetadataStore,
309
215
  ): any {
310
216
  const toolName = originalTool.name;
311
- const label = isMcpToolDefinition(originalTool, toolName)
312
- ? humanizeMcpToolName(toolName)
313
- : originalTool.label || toolName;
217
+ const label = originalTool.label || toolName;
218
+ const defaultTitle = label === toolName ? humanizeToolLabel(label) : label;
219
+ // MCP 工具直接用 adapter 暴露的真实工具名,不做人性化
220
+ const title = mcpToolTitle({ toolName, definition: originalTool }) ?? defaultTitle;
314
221
 
315
222
  return {
316
223
  ...originalTool,
317
224
  renderShell: "self",
318
225
  renderCall(args: any, theme: any, context: any) {
319
- if (config.mode !== "on") {
226
+ if (config.mode === "off") {
320
227
  return renderDefault(originalTool, "renderCall", [args, theme, context], String(toolName));
321
228
  }
322
229
 
@@ -324,31 +231,71 @@ function createCcstyleTool(
324
231
  const isPending =
325
232
  visualState === "pending" ||
326
233
  (!visualState && (context?.isPartial || context?.executionStarted));
327
- if (isPending && context?.executionStarted) scheduleAnimation(context);
328
- const rawIcon = isPending ? pendingIcon(toolName) : settledIcon(toolName, visualState);
329
- const icon =
234
+ const animating = isPending && Boolean(context?.executionStarted);
235
+ if (animating) scheduleAnimation(context, { light: true });
236
+ const settledRawIcon = isPending ? "" : settledIcon(toolName, visualState);
237
+ const settledIconStyled =
330
238
  visualState === "success"
331
- ? `${BRIGHT_GREEN}${rawIcon}${ANSI_FG_RESET}`
332
- : theme.fg(toolIconColor(context), rawIcon);
333
- const summary = singleToolCallSummary(toolName, label, args, context?.cwd);
239
+ ? `${BRIGHT_GREEN}${settledRawIcon}${ANSI_FG_RESET}`
240
+ : theme.fg(toolIconColor(context), settledRawIcon);
241
+ // 逐帧 spinner:帧在 render() 内取,动画定时器只需 requestRender,
242
+ // 不再每次 tick 走 updateDisplay 把整张卡重建一遍。
243
+ const pendingIconStyled = () => theme.fg(toolIconColor(context), pendingIcon(toolName));
244
+ const summary = toolCallSummary(toolName, args, {
245
+ title,
246
+ variant: "default",
247
+ cwd: context?.cwd,
248
+ });
249
+ let writeStatsText = "";
250
+ let writeStatsStyled = "";
251
+ if (toolName === "write" && visualState === "success") {
252
+ const meta = writeExecutionMetadata.get(context?.toolCallId);
253
+ const stats = countWriteDiffStats(
254
+ typeof args?.content === "string" ? args.content : undefined,
255
+ meta?.previousContent,
256
+ meta?.fileExistedBeforeWrite,
257
+ );
258
+ if (stats) {
259
+ writeStatsText = ` (+${stats.added} -${stats.removed})`;
260
+ writeStatsStyled = ` ${theme.fg("dim", "(")}${theme.fg("success", `+${stats.added}`)} ${theme.fg("error", `-${stats.removed}`)}${theme.fg("dim", ")")}`;
261
+ }
262
+ }
263
+ const extraText = writeStatsText || summary.detail;
264
+ const extraStyled = writeStatsStyled || theme.fg("dim", summary.detail);
334
265
  let cachedWidth: number | undefined;
335
266
  let cachedLine: string | undefined;
267
+ let cachedIcon: string | undefined;
268
+ const expanded = Boolean(context?.expanded);
336
269
  return {
337
270
  render(width: number) {
338
- if (cachedLine !== undefined && cachedWidth === width) return [cachedLine];
271
+ // 轻量 tick 只会 requestRender,靠这里续期,动画才能自维持。
272
+ if (animating) scheduleAnimation(context, { light: true });
273
+ const icon = isPending ? pendingIconStyled() : settledIconStyled;
274
+ if (cachedLine !== undefined && cachedWidth === width && cachedIcon === icon) {
275
+ return [cachedLine];
276
+ }
339
277
  const viewportWidth = toolViewportWidth(width);
340
- const callWidth = Math.max(0, viewportWidth - visibleWidth(icon) - 2);
341
- const mainWidth = Math.max(0, callWidth - visibleWidth(summary.detail));
278
+ // 展开态贴左(外层 Box 已 pad 1);折叠 self-shell 保留 1 格前导空格
279
+ const lead = expanded ? "" : " ";
280
+ const callWidth = Math.max(
281
+ 0,
282
+ viewportWidth - visibleWidth(icon) - 1 - (expanded ? 0 : 1),
283
+ );
284
+ const mainWidth = Math.max(0, callWidth - visibleWidth(extraText));
342
285
  cachedWidth = width;
343
- const line = ` ${icon} ${theme.fg("toolTitle", fitToolCallSummary(summary, mainWidth))}${theme.fg("dim", summary.detail)}`;
344
- cachedLine = truncateToWidth(line, viewportWidth, "");
345
- return [cachedLine];
286
+ cachedIcon = icon;
287
+ // 路径按最终可用宽度中间截断,避免整行二次截断隐藏文件名。
288
+ cachedLine = `${lead}${icon} ${renderToolSummary(summary, mainWidth, theme.fg.bind(theme))}${extraStyled}`;
289
+ return [truncateToWidth(cachedLine, viewportWidth, "")];
290
+ },
291
+ invalidate() {
292
+ cachedLine = undefined;
293
+ cachedIcon = undefined;
346
294
  },
347
- invalidate() {},
348
295
  };
349
296
  },
350
297
  renderResult(result: any, options: any, theme: any, context: any) {
351
- if (config.mode !== "on") {
298
+ if (config.mode === "off") {
352
299
  return renderDefault(
353
300
  originalTool,
354
301
  "renderResult",
@@ -357,13 +304,15 @@ function createCcstyleTool(
357
304
  );
358
305
  }
359
306
 
307
+ const expanded = isToolExpanded(options, context);
360
308
  if (options?.isPartial) {
361
- return new Text(theme.fg("muted", " ↳ Pending…"), 0, 0);
309
+ // 展开态贴左(Box 提供 1 格 pad);折叠态保持 3 格对齐标题
310
+ const pending = expanded ? "↳ Pending…" : " ↳ Pending…";
311
+ return new Text(theme.fg("muted", pending), 0, 0);
362
312
  }
363
313
 
364
314
  const isError = options?.isError || context?.isError;
365
315
  setToolVisualState(context, isError ? "error" : "success");
366
- const expanded = isToolExpanded(options, context);
367
316
  const toolCallId = context?.toolCallId;
368
317
  if (shouldRenderRichDiff(config.mode, toolName, Boolean(isError))) {
369
318
  // getter 保证 Diff indicator / wrap / limits 下次绘制即更新
@@ -381,7 +330,8 @@ function createCcstyleTool(
381
330
  writeExecutionMetadata,
382
331
  getToolDisplayConfig,
383
332
  );
384
- if (richResult) return insetComponent(richResult);
333
+ // 展开态由 Box(1,1) 提供内边距;折叠态 inset 一级缩进
334
+ if (richResult) return expanded ? richResult : insetComponent(richResult);
385
335
  }
386
336
 
387
337
  const text = textFromResult(result, expanded);
@@ -398,7 +348,8 @@ function createCcstyleTool(
398
348
  ? taskListSummary(tasks)
399
349
  : isError
400
350
  ? text
401
- ? oneLine(text)
351
+ ? // 72 与原 result.ts 默认一致,保持错误摘要截断宽度。
352
+ oneLine(text, 72)
402
353
  : "Failed"
403
354
  : outputLines
404
355
  ? `${outputLines} ${lineWord} ${action}`
@@ -416,6 +367,7 @@ function createCcstyleTool(
416
367
  context?.lastComponent,
417
368
  args,
418
369
  context,
370
+ true, // mode=on:贴左,由外层 Box(1,1) 提供 1 格 padding
419
371
  );
420
372
  }
421
373
  if (context?.state) context.state.ccstyleIoView = undefined;
@@ -466,19 +418,15 @@ function shouldGloballyStyleTool(component: any, patch: GlobalToolRenderPatch):
466
418
  const builtInDefinition = component.builtInToolDefinition;
467
419
  const definition = extensionDefinition ?? builtInDefinition;
468
420
  const toolName = String(component.toolName || definition?.name || "");
421
+ // compact 也复用同一套 ccstyle call/result,避免折叠态工具卡回落到 Pi 原生样式。
469
422
  const useCcstyle =
470
- patch.mode() === "on" &&
423
+ patch.mode() !== "off" &&
471
424
  !DEDICATED_RENDERER_TOOLS.has(toolName) &&
472
425
  !preservesOriginalRenderer(extensionDefinition, toolName, builtInDefinition);
473
426
  component[COMPONENT_TOOL_RENDER_MODE] = useCcstyle;
474
427
  return useCcstyle;
475
428
  }
476
429
 
477
- function shouldUseSelfShell(component: any, _patch: GlobalToolRenderPatch): boolean {
478
- component[COMPONENT_TOOL_SELF_SHELL_MODE] = false;
479
- return false;
480
- }
481
-
482
430
  function getGloballyStyledTool(component: any, patch: GlobalToolRenderPatch): any {
483
431
  const definition = component.toolDefinition ?? component.builtInToolDefinition;
484
432
  if (definition && typeof definition === "object") {
@@ -516,107 +464,88 @@ function isOwnershipAwarePatch(value: any): value is GlobalToolRenderPatch {
516
464
  );
517
465
  }
518
466
 
519
- function isLegacyInstalledWrapper(method: unknown, downstreamField: string): boolean {
520
- if (typeof method !== "function") return false;
521
- try {
522
- const source = Function.prototype.toString.call(method);
523
- return (
524
- source.includes(downstreamField) &&
525
- (source.includes("shouldGloballyStyleTool") ||
526
- source.includes("shouldUseSelfShell") ||
527
- source.includes("getGloballyStyledTool"))
528
- );
529
- } catch {
530
- return false;
531
- }
532
- }
533
-
534
- function downstreamForGlobalToolInstall(prototype: any, previous: any): ToolRenderMethods {
467
+ function downstreamForGlobalToolInstall(
468
+ prototype: any,
469
+ previous: GlobalToolRenderPatch | undefined,
470
+ ): ToolRenderMethods {
535
471
  const current = prototypeToolRenderMethods(prototype);
536
- if (!previous || previous.prototype !== prototype) return current;
537
- if (isOwnershipAwarePatch(previous)) {
538
- return {
539
- hasRendererDefinition:
540
- current.hasRendererDefinition === previous.installed.hasRendererDefinition
541
- ? previous.downstream.hasRendererDefinition
542
- : current.hasRendererDefinition,
543
- getRenderShell:
544
- current.getRenderShell === previous.installed.getRenderShell
545
- ? previous.downstream.getRenderShell
546
- : current.getRenderShell,
547
- getCallRenderer:
548
- current.getCallRenderer === previous.installed.getCallRenderer
549
- ? previous.downstream.getCallRenderer
550
- : current.getCallRenderer,
551
- getResultRenderer:
552
- current.getResultRenderer === previous.installed.getResultRenderer
553
- ? previous.downstream.getResultRenderer
554
- : current.getResultRenderer,
555
- };
472
+ if (!previous || previous.prototype !== prototype || !isOwnershipAwarePatch(previous)) {
473
+ return current;
556
474
  }
557
-
558
- // pre-v2 Symbol 无 wrapper 引用;能识别则回退,否则保留当前方法为 external。
559
- const legacyDownstream = (method: Function, field: string): Function => {
560
- const saved = previous[field];
561
- return typeof saved === "function" && isLegacyInstalledWrapper(method, field) ? saved : method;
562
- };
563
475
  return {
564
- hasRendererDefinition: legacyDownstream(
565
- current.hasRendererDefinition,
566
- "originalHasRendererDefinition",
567
- ) as ToolRenderMethods["hasRendererDefinition"],
568
- getRenderShell: legacyDownstream(
569
- current.getRenderShell,
570
- "originalGetRenderShell",
571
- ) as ToolRenderMethods["getRenderShell"],
572
- getCallRenderer: legacyDownstream(
573
- current.getCallRenderer,
574
- "originalGetCallRenderer",
575
- ) as ToolRenderMethods["getCallRenderer"],
576
- getResultRenderer: legacyDownstream(
577
- current.getResultRenderer,
578
- "originalGetResultRenderer",
579
- ) as ToolRenderMethods["getResultRenderer"],
476
+ hasRendererDefinition:
477
+ current.hasRendererDefinition === previous.installed.hasRendererDefinition
478
+ ? previous.downstream.hasRendererDefinition
479
+ : current.hasRendererDefinition,
480
+ getRenderShell:
481
+ current.getRenderShell === previous.installed.getRenderShell
482
+ ? previous.downstream.getRenderShell
483
+ : current.getRenderShell,
484
+ getCallRenderer:
485
+ current.getCallRenderer === previous.installed.getCallRenderer
486
+ ? previous.downstream.getCallRenderer
487
+ : current.getCallRenderer,
488
+ getResultRenderer:
489
+ current.getResultRenderer === previous.installed.getResultRenderer
490
+ ? previous.downstream.getResultRenderer
491
+ : current.getResultRenderer,
580
492
  };
581
493
  }
582
494
 
583
- function disconnectGlobalToolRenderPatch(patch: any): void {
584
- if (!patch || typeof patch !== "object") return;
495
+ function disconnectGlobalToolRenderPatch(patch: GlobalToolRenderPatch | undefined): void {
496
+ if (!patch) return;
585
497
  patch.active = false;
586
- patch.enabled = () => false;
587
- patch.mode = () => "off";
588
- patch.wrap = (tool: any) => tool;
589
498
  patch.byDefinition = new WeakMap();
590
- if (patch.byName && typeof patch.byName.clear === "function") patch.byName.clear();
591
- else patch.byName = new Map();
499
+ patch.byName.clear();
500
+ }
501
+
502
+ function ioHoverOf(tool: any): string | null {
503
+ const view = tool?.resultRendererComponent;
504
+ if (!view || typeof view.getHoveredSection !== "function") return null;
505
+ return view.getHoveredSection();
506
+ }
507
+
508
+ function toolPaintMatches(hit: ToolPaintHit, tool: any, width: number): boolean {
509
+ return (
510
+ hit.width === width &&
511
+ hit.expanded === Boolean(tool.expanded) &&
512
+ hit.isPartial === Boolean(tool.isPartial) &&
513
+ hit.result === tool.result &&
514
+ hit.args === tool.args &&
515
+ hit.callHover === isToolCallHovered(tool.toolCallId) &&
516
+ hit.ioHover === ioHoverOf(tool)
517
+ );
592
518
  }
593
519
 
594
520
  function installGlobalToolRendering(
595
521
  writeExecutionMetadata: WriteExecutionMetadataStore,
596
522
  ): GlobalToolRenderPatch {
597
523
  const prototype = (ToolExecutionComponent as any).prototype;
598
- const host = globalThis as any;
599
- const previous = host[GLOBAL_TOOL_RENDER_PATCH];
524
+ const previous = patchRegistry.get<GlobalToolRenderPatch>(GLOBAL_TOOL_RENDER_PATCH);
600
525
  const downstream = downstreamForGlobalToolInstall(prototype, previous);
601
- // 外部仍持有的旧 wrapper 先变为无回调 pass-through,再挂新安装。
602
- disconnectGlobalToolRenderPatch(previous);
526
+ const originalRender =
527
+ previous?.originalRender && prototype.render === previous.renderPaint
528
+ ? previous.originalRender
529
+ : prototype.render;
530
+ const originalInvalidate =
531
+ previous?.originalInvalidate && prototype.invalidate === previous.invalidatePaint
532
+ ? previous.originalInvalidate
533
+ : prototype.invalidate;
534
+ if (isOwnershipAwarePatch(previous)) disconnectGlobalToolRenderPatch(previous);
603
535
 
604
536
  const patch: GlobalToolRenderPatch = {
605
537
  version: 2,
606
538
  prototype,
607
- owner: {},
608
539
  active: true,
609
- enabled: () => config.mode === "on",
610
540
  mode: () => config.mode,
611
541
  wrap: (tool: any) => createCcstyleTool(tool, writeExecutionMetadata),
612
542
  byDefinition: new WeakMap(),
613
543
  byName: new Map(),
614
544
  downstream,
615
545
  installed: undefined as any,
616
- originalHasRendererDefinition: downstream.hasRendererDefinition,
617
- originalGetRenderShell: downstream.getRenderShell,
618
- originalGetCallRenderer: downstream.getCallRenderer,
619
- originalGetResultRenderer: downstream.getResultRenderer,
546
+ originalRender,
547
+ originalInvalidate,
548
+ paintCache: new WeakMap(),
620
549
  };
621
550
 
622
551
  patch.installed = {
@@ -626,14 +555,12 @@ function installGlobalToolRendering(
626
555
  },
627
556
  getRenderShell: function (this: any, ...args: any[]) {
628
557
  if (!patch.active) return patch.downstream.getRenderShell.apply(this, args);
629
- const useSelfShell = shouldUseSelfShell(this, patch);
630
558
  const useCcstyle = shouldGloballyStyleTool(this, patch);
631
- const shell =
632
- useSelfShell || (useCcstyle && !this.expanded)
633
- ? "self"
634
- : useCcstyle
635
- ? "default"
636
- : patch.downstream.getRenderShell.apply(this, args);
559
+ const shell = useCcstyle
560
+ ? this.expanded
561
+ ? "default"
562
+ : "self"
563
+ : patch.downstream.getRenderShell.apply(this, args);
637
564
  syncToolShell(this, shell);
638
565
  return shell;
639
566
  },
@@ -655,7 +582,42 @@ function installGlobalToolRendering(
655
582
  prototype.getRenderShell = patch.installed.getRenderShell;
656
583
  prototype.getCallRenderer = patch.installed.getCallRenderer;
657
584
  prototype.getResultRenderer = patch.installed.getResultRenderer;
658
- host[GLOBAL_TOOL_RENDER_PATCH] = patch;
585
+ // fullscreen 滚动/选区每帧整树 render;内容未变时复用上一帧行,避免重做 diff/Box。
586
+ patch.renderPaint = function (this: any, width: number): string[] {
587
+ if (!patch.active || patch.mode() === "off") {
588
+ return originalRender.call(this, width);
589
+ }
590
+ const cache = patch.paintCache;
591
+ // A running partial tool owns a time-varying spinner. Light animation ticks
592
+ // request a repaint without invalidating the component, so settled-content
593
+ // caching must not intercept those paints.
594
+ const cacheable = !(this.executionStarted && this.isPartial);
595
+ const hit = cacheable ? cache?.get(this) : undefined;
596
+ if (hit && toolPaintMatches(hit, this, width)) return hit.lines;
597
+ const lines = originalRender.call(this, width);
598
+ if (cacheable) {
599
+ cache?.set(this, {
600
+ width,
601
+ expanded: Boolean(this.expanded),
602
+ isPartial: Boolean(this.isPartial),
603
+ result: this.result,
604
+ args: this.args,
605
+ callHover: isToolCallHovered(this.toolCallId),
606
+ ioHover: ioHoverOf(this),
607
+ lines,
608
+ });
609
+ } else {
610
+ cache?.delete(this);
611
+ }
612
+ return lines;
613
+ };
614
+ patch.invalidatePaint = function (this: any): void {
615
+ patch.paintCache?.delete(this);
616
+ originalInvalidate.call(this);
617
+ };
618
+ prototype.render = patch.renderPaint;
619
+ prototype.invalidate = patch.invalidatePaint;
620
+ patchRegistry.install(GLOBAL_TOOL_RENDER_PATCH, patch);
659
621
  return patch;
660
622
  }
661
623
 
@@ -675,15 +637,23 @@ function deactivateGlobalToolRendering(patch: GlobalToolRenderPatch): void {
675
637
  if (prototype.getResultRenderer === patch.installed.getResultRenderer) {
676
638
  prototype.getResultRenderer = patch.downstream.getResultRenderer;
677
639
  }
640
+ if (patch.renderPaint && prototype.render === patch.renderPaint && patch.originalRender) {
641
+ prototype.render = patch.originalRender;
642
+ }
643
+ if (
644
+ patch.invalidatePaint &&
645
+ prototype.invalidate === patch.invalidatePaint &&
646
+ patch.originalInvalidate
647
+ ) {
648
+ prototype.invalidate = patch.originalInvalidate;
649
+ }
678
650
  }
679
651
 
680
652
  /** 展开面板背景统一为 user message 背景色;折叠行保持原生状态色。
681
653
  * 必须在 compact-mode 之后安装,shutdown 时先于 compact-mode 释放。 */
682
654
  export function installToolExpandedBackground(): () => void {
683
- const host = globalThis as any;
684
- const previous = host[TOOL_EXPANDED_BACKGROUND_PATCH] as ToolExpandedBackgroundPatch | undefined;
655
+ const previous = patchRegistry.get<ToolExpandedBackgroundPatch>(TOOL_EXPANDED_BACKGROUND_PATCH);
685
656
  if (previous) previous.dispose();
686
- // SAFETY: Pi's runtime ToolExecutionComponent prototype owns updateDisplay; its public type omits it.
687
657
  const prototype = ToolExecutionComponent.prototype as unknown as { updateDisplay: () => void };
688
658
  const original = prototype.updateDisplay;
689
659
  const patch: ToolExpandedBackgroundPatch = {
@@ -695,7 +665,12 @@ export function installToolExpandedBackground(): () => void {
695
665
  const theme = getMessageDisplayTheme();
696
666
  if (!theme?.bg) return;
697
667
  const box = this.contentBox;
698
- if (box?.setBgFn) box.setBgFn((text: string) => theme.bg("userMessageBg", text));
668
+ // 展开最外层卡片:上下左右内间距 1 格
669
+ if (box) {
670
+ box.paddingX = 1;
671
+ box.paddingY = 1;
672
+ if (box.setBgFn) box.setBgFn((text: string) => theme.bg("userMessageBg", text));
673
+ }
699
674
  },
700
675
  original,
701
676
  dispose: () => {
@@ -703,13 +678,11 @@ export function installToolExpandedBackground(): () => void {
703
678
  if (prototype.updateDisplay === patch.installed) {
704
679
  prototype.updateDisplay = original;
705
680
  }
706
- if (host[TOOL_EXPANDED_BACKGROUND_PATCH] === patch) {
707
- delete host[TOOL_EXPANDED_BACKGROUND_PATCH];
708
- }
681
+ patchRegistry.dispose(TOOL_EXPANDED_BACKGROUND_PATCH, patch);
709
682
  },
710
683
  };
711
684
  prototype.updateDisplay = patch.installed;
712
- host[TOOL_EXPANDED_BACKGROUND_PATCH] = patch;
685
+ patchRegistry.install(TOOL_EXPANDED_BACKGROUND_PATCH, patch);
713
686
  return patch.dispose;
714
687
  }
715
688
 
@@ -721,7 +694,7 @@ export function installDefaultMode(
721
694
  return {
722
695
  isOwner() {
723
696
  return (
724
- (globalThis as any)[GLOBAL_TOOL_RENDER_PATCH] === globalToolRendering &&
697
+ patchRegistry.owns(GLOBAL_TOOL_RENDER_PATCH, globalToolRendering) &&
725
698
  globalToolRendering.active
726
699
  );
727
700
  },