@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
@@ -4,8 +4,10 @@ import {
4
4
  SkillInvocationMessageComponent,
5
5
  } from "@earendil-works/pi-coding-agent";
6
6
  import { Markdown, Spacer, Text } from "@earendil-works/pi-tui";
7
- import { config } from "../config/config.ts";
7
+ import { MESSAGE_DISPLAY_PATCH, patchRegistry } from "../../utils/patch-keys.ts";
8
+ import { config } from "../../config/config.ts";
8
9
  import { showMoreHintText } from "./show-more-hint.ts";
10
+ import { walkComponentTree } from "../../utils/component-tree.ts";
9
11
 
10
12
  /**
11
13
  * 接管三个消息组件(`<skill>` 块、压缩摘要、分支摘要),ccstyle on 时渲染为
@@ -16,11 +18,6 @@ import { showMoreHintText } from "./show-more-hint.ts";
16
18
  * ToolExecutionComponent,无法复用其原型 patch,这里单独统一管理。
17
19
  */
18
20
 
19
- const MESSAGE_DISPLAY_PATCH = Symbol.for("pi.ccstyle.message-display-patch");
20
- const MESSAGE_DISPLAY_ORIGINAL_BG = Symbol.for("pi.ccstyle.message-display-original-bg");
21
- const MESSAGE_DISPLAY_ORIGINAL_PADDING = Symbol.for("pi.ccstyle.message-display-original-padding");
22
- const MESSAGE_DISPLAY_ORIGINAL_SAVED = Symbol.for("pi.ccstyle.message-display-original-saved");
23
-
24
21
  // 与 renderer/index.ts renderCall 的成功勾一致:亮绿 ✓(truecolor ANSI)。
25
22
  const BRIGHT_GREEN = "\x1b[38;2;80;220;100m";
26
23
  const ANSI_FG_RESET = "\x1b[39m";
@@ -67,30 +64,14 @@ function ensureHintHover(component: any): void {
67
64
  };
68
65
  }
69
66
 
70
- function rememberMessageDisplay(component: any): void {
71
- if (component[MESSAGE_DISPLAY_ORIGINAL_SAVED]) return;
72
- component[MESSAGE_DISPLAY_ORIGINAL_BG] = component.bgFn;
73
- component[MESSAGE_DISPLAY_ORIGINAL_PADDING] = [component.paddingX, component.paddingY];
74
- component[MESSAGE_DISPLAY_ORIGINAL_SAVED] = true;
75
- }
76
-
77
- function restoreMessageDisplay(component: any): void {
78
- if (!component[MESSAGE_DISPLAY_ORIGINAL_SAVED]) return;
79
- const [paddingX, paddingY] = component[MESSAGE_DISPLAY_ORIGINAL_PADDING];
80
- component.paddingX = paddingX;
81
- component.paddingY = paddingY;
82
- component.setBgFn?.(component[MESSAGE_DISPLAY_ORIGINAL_BG]);
83
- delete component[MESSAGE_DISPLAY_ORIGINAL_BG];
84
- delete component[MESSAGE_DISPLAY_ORIGINAL_PADDING];
85
- delete component[MESSAGE_DISPLAY_ORIGINAL_SAVED];
86
- }
87
-
88
- function renderCcstyle(component: any, kind: DisplayKind): boolean {
67
+ function renderCcstyle(component: any, kind: DisplayKind): void {
89
68
  const theme = displayTheme;
90
- if (!theme) return false; // 主题未就绪时回退原生渲染
91
- rememberMessageDisplay(component);
69
+ if (!theme) return; // 主题未就绪时保留原生渲染
92
70
  if (component.bgFn) component.setBgFn?.(undefined); // 与工具调用一致,去掉灰底
93
- component.paddingY = 0; // 工具组件 paddingY=0;原生 Box 默认 1,会上下各留一个空行
71
+ if (component.paddingY !== 0) {
72
+ component._ccstyleOriginalPaddingY = component.paddingY;
73
+ component.paddingY = 0; // 工具组件 paddingY=0;原生 Box 默认 1,会上下各留一个空行
74
+ }
94
75
  component.clear();
95
76
  ensureHintHover(component);
96
77
  const icon = `${BRIGHT_GREEN}✓${ANSI_FG_RESET}`; // 已完成消息,等同工具成功态
@@ -99,7 +80,7 @@ function renderCcstyle(component: any, kind: DisplayKind): boolean {
99
80
  const hovered = component.hintHovered === true;
100
81
  const hint = `${theme.fg("dim", " • ")}${theme.fg(hovered ? "text" : "dim", showMoreHintText())}`;
101
82
  component.addChild(new Text(`${icon} ${title}${hint}`, 0, 0));
102
- return true;
83
+ return;
103
84
  }
104
85
  // 展开卡与 tool 一致:userMessageBg + 上下左右 1 格
105
86
  component.paddingX = 1;
@@ -114,7 +95,6 @@ function renderCcstyle(component: any, kind: DisplayKind): boolean {
114
95
  color: (text: string) => theme.fg("customMessageText", text),
115
96
  }),
116
97
  );
117
- return true;
118
98
  }
119
99
 
120
100
  type PatchEntry = {
@@ -125,45 +105,30 @@ type PatchEntry = {
125
105
 
126
106
  /** patch 三个消息组件的 updateDisplay,返回统一 dispose(/reload 链安全)。 */
127
107
  export function installMessageDisplayRendering(): () => void {
128
- const host = globalThis as any;
129
- const previous = host[MESSAGE_DISPLAY_PATCH];
108
+ const previous = patchRegistry.get<{ dispose: () => void }>(MESSAGE_DISPLAY_PATCH);
130
109
  if (previous) previous.dispose();
131
- const patch: {
132
- active: boolean;
133
- entries: PatchEntry[];
134
- components: Set<WeakRef<any>>;
135
- tracked: WeakSet<object>;
136
- dispose: () => void;
137
- } = {
110
+ const patch: { active: boolean; entries: PatchEntry[]; dispose: () => void } = {
138
111
  active: true,
139
112
  entries: [],
140
- components: new Set(),
141
- tracked: new WeakSet(),
142
113
  dispose: () => {},
143
114
  };
144
- const track = (component: Record<PropertyKey, unknown>): void => {
145
- for (const ref of patch.components) {
146
- if (!ref.deref()) patch.components.delete(ref);
147
- }
148
- if (patch.tracked.has(component)) return;
149
- patch.tracked.add(component);
150
- patch.components.add(new WeakRef(component));
151
- };
152
115
  const installOne = (ComponentClass: any, kind: DisplayKind): void => {
153
116
  const prototype = ComponentClass.prototype;
154
117
  const original = prototype.updateDisplay;
155
118
  const installed = function (this: any) {
156
119
  if (patch.active && config.mode !== "off") {
157
120
  try {
158
- if (renderCcstyle(this, kind)) {
159
- track(this);
160
- return;
161
- }
121
+ renderCcstyle(this, kind);
122
+ return;
162
123
  } catch {
163
124
  // 渲染失败回退原生
164
125
  }
165
126
  }
166
- restoreMessageDisplay(this);
127
+ // 回退原生前恢复 paddingY,避免原生渲染丢失上下内边距
128
+ if (this._ccstyleOriginalPaddingY !== undefined) {
129
+ this.paddingY = this._ccstyleOriginalPaddingY;
130
+ delete this._ccstyleOriginalPaddingY;
131
+ }
167
132
  original.call(this);
168
133
  };
169
134
  prototype.updateDisplay = installed;
@@ -179,20 +144,9 @@ export function installMessageDisplayRendering(): () => void {
179
144
  entry.prototype.updateDisplay = entry.original;
180
145
  }
181
146
  }
182
- for (const ref of patch.components) {
183
- const component = ref.deref();
184
- if (!component) continue;
185
- try {
186
- restoreMessageDisplay(component);
187
- component.updateDisplay();
188
- } catch {
189
- // 单个陈旧组件不应阻断其余原型与组件恢复
190
- }
191
- }
192
- patch.components.clear();
193
- if (host[MESSAGE_DISPLAY_PATCH] === patch) delete host[MESSAGE_DISPLAY_PATCH];
147
+ patchRegistry.dispose(MESSAGE_DISPLAY_PATCH, patch);
194
148
  };
195
- host[MESSAGE_DISPLAY_PATCH] = patch;
149
+ patchRegistry.install(MESSAGE_DISPLAY_PATCH, patch);
196
150
  return patch.dispose;
197
151
  }
198
152
 
@@ -210,30 +164,10 @@ export function isMessageDisplayComponent(value: any): boolean {
210
164
 
211
165
  /** 遍历当前 transcript,让已挂载的消息组件按当前 mode 重渲染(/ccstyle on|off 切换)。 */
212
166
  export function refreshMessageDisplays(root: any): void {
213
- const seen = new Set<any>();
214
- const visit = (value: any): void => {
215
- if (!value || typeof value !== "object" || seen.has(value)) return;
216
- seen.add(value);
217
- if (Array.isArray(value)) {
218
- for (const child of value) visit(child);
219
- return;
220
- }
167
+ walkComponentTree(root, (value: any) => {
221
168
  if (isMessageDisplayComponent(value)) {
222
169
  value.invalidate?.();
223
- return;
170
+ return false;
224
171
  }
225
- const children = value.children;
226
- if (Array.isArray(children)) {
227
- for (const child of children) visit(child);
228
- }
229
- try {
230
- const mounted = value.getMountedRoots?.();
231
- if (Array.isArray(mounted)) {
232
- for (const root of mounted) visit(root);
233
- }
234
- } catch {
235
- // 惰性 Proxy 可能暂时没有 mounted roots
236
- }
237
- };
238
- visit(root);
172
+ });
239
173
  }
@@ -0,0 +1,426 @@
1
+ import { posix, win32 } from "node:path";
2
+ import type { ThemeColor } from "@earendil-works/pi-coding-agent";
3
+ import { visibleWidth } from "@earendil-works/pi-tui";
4
+ import { config } from "../../config/config.ts";
5
+ import { oneLine } from "../../utils/format.ts";
6
+ import { headTruncateToWidth } from "./result.ts";
7
+
8
+ /** inputClip=0 时不设字符上限,交给渲染时的实际宽度截断。 */
9
+ function clipLimit(): number {
10
+ return config.inputClip > 0 ? config.inputClip : Number.POSITIVE_INFINITY;
11
+ }
12
+
13
+ function clip(value: unknown): string {
14
+ return oneLine(value, clipLimit());
15
+ }
16
+
17
+ /** 载荷(入参 JSON / 脚本代码)至少留出这么多宽度才显示,否则整段省略(避免只剩一个孤零零的 " {…")。 */
18
+ const MIN_PAYLOAD_WIDTH = 10;
19
+
20
+ /** 品牌大小写固定写法;humanize 推导不出来的工具名写在这里。 */
21
+ const TOOL_LABEL_OVERRIDES: Readonly<Record<string, string>> = {
22
+ powershell: "PowerShell",
23
+ // MCP 入口两个没有具体工具名,跟普通工具一样人性化;缩写固定大写
24
+ mcp: "MCP",
25
+ mcpscript: "MCP Script",
26
+ };
27
+
28
+ /**
29
+ * 工具名/标签人性化:与 default-mode 的 humanizeToolLabel、grouping 的 humanizeToolName
30
+ * 逐字相同,收敛为一个共享实现。
31
+ */
32
+ export function humanizeToolLabel(label: string): string {
33
+ const brand = TOOL_LABEL_OVERRIDES[label.toLowerCase()];
34
+ if (brand) return brand;
35
+ return label
36
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
37
+ .replace(/[_-]+/g, " ")
38
+ .replace(/\b\w/g, (char) => char.toUpperCase());
39
+ }
40
+
41
+ const AGENT_FAMILY_TOOL_NAMES = new Set([
42
+ "Agent",
43
+ "Agents",
44
+ "get_subagent_result",
45
+ "steer_subagent",
46
+ ]);
47
+
48
+ /** default-mode(单工具卡)与 grouping(分组卡)在 agent/bash/grep/find/read/fallback 文案上不同。 */
49
+ export type ToolCallSummaryVariant = "default" | "grouping";
50
+
51
+ export type ToolCallSummaryOptions = {
52
+ /** 已解析标题;缺省为 humanizeToolLabel(toolName)。 */
53
+ title?: string;
54
+ /** 文案变体;缺省 "default"。 */
55
+ variant?: ToolCallSummaryVariant;
56
+ /** 工具执行目录;仅用于生成展示路径,不修改调用参数。 */
57
+ cwd?: string;
58
+ };
59
+
60
+ export type ToolCallSummary = {
61
+ main: string;
62
+ detail: string;
63
+ /** 路径摘要保留结构,供最终渲染按实际宽度优先保留文件名。 */
64
+ path?: { prefix: string; value: string };
65
+ /** 标题之外的原始载荷(完整入参 JSON、脚本代码…),已压成单行,渲染时用 dim 接在标题后。 */
66
+ payload?: string;
67
+ };
68
+
69
+ function pathApi(value: string) {
70
+ if (posix.isAbsolute(value)) return posix;
71
+ if (win32.isAbsolute(value)) return win32;
72
+ return undefined;
73
+ }
74
+
75
+ /** cwd 内绝对路径转相对路径;cwd 外路径保持不变。 */
76
+ export function displayPath(value: unknown, cwd?: string): string {
77
+ const text = oneLine(value, 4096);
78
+ const base = cwd ? oneLine(cwd, 4096) : "";
79
+ const api = pathApi(text);
80
+ if (!api || !base || !api.isAbsolute(base)) return text;
81
+ const relative = api.relative(base, text);
82
+ if (!relative) return api.basename(text) || ".";
83
+ if (relative === ".." || relative.startsWith(`..${api.sep}`) || api.isAbsolute(relative)) {
84
+ return text;
85
+ }
86
+ return relative;
87
+ }
88
+
89
+ function headToWidth(text: string, width: number, ellipsis = ""): string {
90
+ if (visibleWidth(text) <= width) return text;
91
+ if (width <= 0) return "";
92
+ const suffix = visibleWidth(ellipsis) <= width ? ellipsis : "";
93
+ const contentWidth = width - visibleWidth(suffix);
94
+ let head = "";
95
+ for (const char of Array.from(text)) {
96
+ if (visibleWidth(head + char) > contentWidth) break;
97
+ head += char;
98
+ }
99
+ return head + suffix;
100
+ }
101
+
102
+ function tailToWidth(text: string, width: number): string {
103
+ if (width <= 0) return "";
104
+ let tail = "";
105
+ for (const char of Array.from(text).reverse()) {
106
+ if (visibleWidth(char + tail) > width) break;
107
+ tail = char + tail;
108
+ }
109
+ return tail;
110
+ }
111
+
112
+ /** 中间截断路径:目录保留开头,末尾优先完整保留文件名。 */
113
+ export function truncatePathToWidth(path: string, width: number): string {
114
+ if (visibleWidth(path) <= width) return path;
115
+ if (width <= 1) return width === 1 ? "…" : "";
116
+ const separatorIndex = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
117
+ const filename = separatorIndex >= 0 ? path.slice(separatorIndex + 1) : path;
118
+ const filenameWidth = visibleWidth(filename);
119
+ if (separatorIndex >= 0 && filenameWidth + 1 <= width) {
120
+ const separator = path[separatorIndex]!;
121
+ const suffix = `${separator}${filename}`;
122
+ const prefixWidth = width - visibleWidth(suffix) - 1;
123
+ const prefix = headToWidth(path.slice(0, separatorIndex), Math.max(0, prefixWidth));
124
+ return `${prefix}…${prefix ? suffix : filename}`;
125
+ }
126
+ const leftWidth = Math.max(1, Math.floor((width - 1) * 0.45));
127
+ const rightWidth = Math.max(0, width - leftWidth - 1);
128
+ return `${headToWidth(filename, leftWidth)}…${tailToWidth(filename, rightWidth)}`;
129
+ }
130
+
131
+ /** 路径展示的统一入口:相对化后按配置和当前可用宽度截断。 */
132
+ export function formatDisplayPath(value: unknown, cwd: string | undefined, width: number): string {
133
+ return truncatePathToWidth(displayPath(value, cwd), Math.min(width, clipLimit()));
134
+ }
135
+
136
+ /** 按最终终端宽度渲染摘要;路径摘要不会再被整行头部截断。 */
137
+ export function fitToolCallSummary(summary: ToolCallSummary, width: number): string {
138
+ if (!summary.path) return headToWidth(summary.main, width, "…");
139
+ const prefix = summary.path.prefix;
140
+ const pathWidth = Math.max(0, width - visibleWidth(prefix) - 1);
141
+ if (pathWidth <= 0) return headToWidth(prefix, width, "…");
142
+ return `${prefix} ${truncatePathToWidth(summary.path.value, Math.min(pathWidth, clipLimit()))}`;
143
+ }
144
+
145
+ function pathSummary(
146
+ prefix: string,
147
+ value: unknown,
148
+ cwd: string | undefined,
149
+ detail = "",
150
+ ): ToolCallSummary {
151
+ const path = displayPath(value, cwd);
152
+ return {
153
+ main: `${prefix} ${truncatePathToWidth(path, clipLimit())}`,
154
+ detail,
155
+ path: { prefix, value: path },
156
+ };
157
+ }
158
+
159
+ /** 入参是 shell 命令的工具。 */
160
+ const SHELL_TOOL_NAMES = new Set(["bash", "powershell", "pwsh", "sh", "zsh", "cmd"]);
161
+
162
+ /** 内置检索工具:pattern 缺失(无效调用)时保留 "…" 占位。 */
163
+ const SEARCH_TOOL_NAMES = new Set(["grep", "find"]);
164
+
165
+ /**
166
+ * 通用摘要字段优先级:default 与 grouping 共用同一份取值链。
167
+ * 两处曾各自维护,grouping 因此漏掉 command(powershell)和数组型入参;
168
+ * 路径不在链上,由 pathSummary 兜底,以便按 cwd 相对化并中间截断。
169
+ */
170
+ const GENERIC_SUMMARY_FIELDS = [
171
+ "agent_id",
172
+ "command",
173
+ "query",
174
+ "queries",
175
+ "claim",
176
+ "question",
177
+ "questions",
178
+ "url",
179
+ "urls",
180
+ "responseId",
181
+ "findText",
182
+ "name",
183
+ "tool_use_id",
184
+ "toolCallId",
185
+ "id",
186
+ "subject",
187
+ "taskId",
188
+ "task_id",
189
+ "message",
190
+ "description",
191
+ "prompt",
192
+ ] as const;
193
+
194
+ /** 通用字段链取值:字符串直接用;数组取首个可展示项并标注剩余条数。 */
195
+ type SummaryValue = { text: string; more: number };
196
+
197
+ function arrayItemText(item: unknown): string | undefined {
198
+ if (typeof item === "string" && item) return item;
199
+ if (!item || typeof item !== "object") return undefined;
200
+ for (const key of ["query", "url", "question", "header", "name"] as const) {
201
+ const text = (item as Record<string, unknown>)[key];
202
+ if (typeof text === "string" && text) return text;
203
+ }
204
+ return undefined;
205
+ }
206
+
207
+ function summaryFieldValue(value: unknown): SummaryValue | undefined {
208
+ if (typeof value === "string" && value) return { text: value, more: 0 };
209
+ if (!Array.isArray(value)) return undefined;
210
+ let first: string | undefined;
211
+ let count = 0;
212
+ for (const item of value) {
213
+ const text = arrayItemText(item);
214
+ if (!text) continue;
215
+ count += 1;
216
+ first ??= text;
217
+ }
218
+ return first === undefined ? undefined : { text: first, more: count - 1 };
219
+ }
220
+
221
+ /** 通用字段链取值;无可用字段时返回 undefined,交给调用方继续降级。 */
222
+ function genericSummary(title: string, args: any): ToolCallSummary | undefined {
223
+ for (const key of GENERIC_SUMMARY_FIELDS) {
224
+ const found = summaryFieldValue(args[key]);
225
+ if (!found) continue;
226
+ // (+N) 追加在截断之后,避免首个元素过长时把条数挤没
227
+ const more = found.more > 0 ? ` (+${found.more})` : "";
228
+ return { main: `${title} ${clip(found.text)}${more}`, detail: "" };
229
+ }
230
+ return undefined;
231
+ }
232
+
233
+ /**
234
+ * 单工具调用摘要({ main, detail })。
235
+ *
236
+ * 两个变体共用同一份取值链,只有 read/agent/skill/task 等具名分支存在必要差异。
237
+ */
238
+ export function toolCallSummary(
239
+ toolName: string,
240
+ args: any,
241
+ opts: ToolCallSummaryOptions = {},
242
+ ): ToolCallSummary {
243
+ const title = opts.title ?? humanizeToolLabel(toolName);
244
+ const variant = opts.variant ?? "default";
245
+ if (!args || typeof args !== "object") return { main: title, detail: "" };
246
+ const name = toolName.toLowerCase();
247
+
248
+ // MCP 入口沿用 mcp-adapter 自己的标题风格(adapter 的 formatMcpProxyToolCallLines):
249
+ // `MCP <动作> <目标>` / `MCP Script <代码>`,比把整包参数摊成 JSON 好读
250
+ if (name === "mcp") {
251
+ const gateway = mcpGatewaySummary(title, args);
252
+ if (gateway) return gateway;
253
+ }
254
+ if (name === "mcpscript") {
255
+ const code = typeof args.code === "string" && args.code ? clip(args.code) : "";
256
+ return code ? { main: title, detail: "", payload: code } : { main: title, detail: "" };
257
+ }
258
+
259
+ const value = (fallback: string, ...keys: string[]) => {
260
+ const found = keys.map((key) => args[key]).find((item) => typeof item === "string" && item);
261
+ return `${title} ${clip(found || fallback)}`;
262
+ };
263
+
264
+ if (variant === "default" && AGENT_FAMILY_TOOL_NAMES.has(toolName) && args.agent_id) {
265
+ return { main: `${title} ${clip(args.agent_id)}`, detail: "" };
266
+ }
267
+ if (variant === "grouping" && (name === "agent" || name === "agents")) {
268
+ const displayName = args.subagent_type ?? args.agent_type ?? args.agent;
269
+ if (typeof displayName === "string" && displayName) {
270
+ return { main: `${title} ${displayName}`, detail: "" };
271
+ }
272
+ return {
273
+ main: value(name === "agent" ? "launch agent" : "launch agents", "description", "prompt"),
274
+ detail: "",
275
+ };
276
+ }
277
+ if (variant === "grouping" && (name === "get_subagent_result" || name === "steer_subagent")) {
278
+ return {
279
+ main: value(name === "get_subagent_result" ? "agent result" : "steer agent", "agent_id"),
280
+ detail: "",
281
+ };
282
+ }
283
+ if (variant === "default" && name === "agents") {
284
+ return { main: value("launch agents", "description", "prompt"), detail: "" };
285
+ }
286
+ if (name === "skill") return { main: value("run skill", "name"), detail: "" };
287
+ if (name === "enterplanmode" || name === "enter_plan_mode") {
288
+ return { main: `${title} enable read-only planning`, detail: "" };
289
+ }
290
+ if (name === "exitplanmode" || name === "exit_plan_mode") {
291
+ return { main: `${title} present plan`, detail: "" };
292
+ }
293
+ if (name === "taskcreate") return { main: value("create task", "subject"), detail: "" };
294
+ if (name === "tasklist") return { main: `${title} task list`, detail: "" };
295
+ if (name === "taskget" || name === "taskupdate") {
296
+ return { main: value("task", "taskId", "task_id"), detail: "" };
297
+ }
298
+ if (name === "taskoutput" || name === "taskstop") {
299
+ return { main: value("background task", "task_id", "taskId"), detail: "" };
300
+ }
301
+ if (name === "taskexecute") {
302
+ const ids = Array.isArray(args.task_ids)
303
+ ? args.task_ids
304
+ : Array.isArray(args.taskIds)
305
+ ? args.taskIds
306
+ : [];
307
+ const summary = ids.length
308
+ ? `${ids[0]}${ids.length > 1 ? ` (+${ids.length - 1} tasks)` : ""}`
309
+ : "start tasks";
310
+ return { main: `${title} ${summary}`, detail: "" };
311
+ }
312
+ if (toolName === "read") {
313
+ const details = [
314
+ args.offset !== undefined ? `offset=${args.offset}` : "",
315
+ args.limit !== undefined ? `limit=${args.limit}` : "",
316
+ ].filter(Boolean);
317
+ const detail = details.length ? ` (${details.join(", ")})` : "";
318
+ if (typeof args.path === "string" && args.path) {
319
+ return pathSummary(variant === "grouping" ? "Read" : title, args.path, opts.cwd, detail);
320
+ }
321
+ return {
322
+ main: variant === "grouping" ? "Read ..." : title,
323
+ detail,
324
+ };
325
+ }
326
+ if (variant === "grouping" && SHELL_TOOL_NAMES.has(name)) {
327
+ // 分组行缺 command 时保留占位,避免与单工具卡一样只剩标题
328
+ return { main: value("...", "command"), detail: "" };
329
+ }
330
+
331
+ // 检索类:pattern 是正文,path 只是范围;两个变体同格式
332
+ if (typeof args.pattern === "string" || SEARCH_TOOL_NAMES.has(name)) {
333
+ const pattern = clip(args.pattern || "...");
334
+ const scope = typeof args.path === "string" && args.path ? ` in ${clip(args.path)}` : "";
335
+ return { main: `${title} ${JSON.stringify(pattern)}${scope}`, detail: "" };
336
+ }
337
+
338
+ const generic = genericSummary(title, args);
339
+ if (generic) return generic;
340
+
341
+ const preferredPath = args.path ?? args.file_path;
342
+ if (typeof preferredPath === "string" && preferredPath) {
343
+ return pathSummary(title, preferredPath, opts.cwd);
344
+ }
345
+
346
+ // 字段链认不出的参数(命名空间代理的 tool+args、第三方工具的自定义键…)展开完整入参 JSON,
347
+ // 而不是只剩一个标题;网关与脚本的专属形状在上面处理
348
+ const payload = jsonArgs(args);
349
+ return payload ? { main: title, detail: "", payload } : { main: title, detail: "" };
350
+ }
351
+
352
+ /**
353
+ * MCP 网关注解 `mcp <动作> <目标>`:动作与目标照搬 mcp-adapter 的 formatMcpProxyToolCallLines,
354
+ * 人眼读起来比参数 JSON 快;`instructions` 是 adapter 自己漏掉的一档(它那里会落到 mcp status)。
355
+ * 认不出的入参返回 undefined,交给通用 JSON 回退,不猜成 status。
356
+ */
357
+ function mcpGatewaySummary(title: string, args: any): ToolCallSummary | undefined {
358
+ const text = (value: unknown) => (typeof value === "string" && value ? value : "");
359
+ const tool = text(args.tool);
360
+ const connect = text(args.connect);
361
+ const describe = text(args.describe);
362
+ const instructions = text(args.instructions);
363
+ const search = text(args.search);
364
+ const server = text(args.server);
365
+ const action = text(args.action);
366
+ const scoped = (target: string) => (server ? `${target} @ ${server}` : target);
367
+
368
+ if (action === "ui-messages") return { main: `${title} ${action}`, detail: "" };
369
+ if (tool) {
370
+ const main = `${title} call ${scoped(tool)}`;
371
+ const payload = innerArgsPayload(args.args);
372
+ return payload ? { main, detail: "", payload } : { main, detail: "" };
373
+ }
374
+ if (connect) return { main: `${title} connect ${connect}`, detail: "" };
375
+ if (describe) return { main: `${title} describe ${scoped(describe)}`, detail: "" };
376
+ if (instructions) return { main: `${title} instructions ${instructions}`, detail: "" };
377
+ if (search) {
378
+ const flags = [
379
+ args.regex === true ? "regex" : "",
380
+ args.includeSchemas === false ? "schemas hidden" : "",
381
+ ].filter(Boolean);
382
+ return {
383
+ main: `${title} search ${scoped(search)}`,
384
+ detail: flags.length ? ` (${flags.join(", ")})` : "",
385
+ };
386
+ }
387
+ if (server) return { main: `${title} list ${server}`, detail: "" };
388
+ if (action) return { main: `${title} ${action}`, detail: "" };
389
+ if (Object.keys(args).length === 0) return { main: `${title} status`, detail: "" };
390
+ return undefined;
391
+ }
392
+
393
+ /** 网关 call 的内层工具入参:对象转单行 JSON;网关允许传 JSON 字符串,就原样取用不再转义。 */
394
+ function innerArgsPayload(value: unknown): string {
395
+ if (typeof value === "string") return value ? clip(value) : "";
396
+ if (!value || typeof value !== "object") return "";
397
+ return jsonArgs(value);
398
+ }
399
+
400
+ /** 入参序列化成单行 JSON;空对象或不可序列化时返回 ""。 */
401
+ function jsonArgs(args: any): string {
402
+ try {
403
+ const json = JSON.stringify(args);
404
+ return json && json !== "{}" ? clip(json) : "";
405
+ } catch {
406
+ return "";
407
+ }
408
+ }
409
+
410
+ /**
411
+ * 标题行内容:标题用调用方的 toolTitle 色,原始载荷(入参 JSON / 脚本代码)单独用 dim。
412
+ * 标题优先占宽(它是身份),剩下的宽度给载荷;载荷超宽时尾部省略,连
413
+ * MIN_PAYLOAD_WIDTH 都放不下时整段省略。
414
+ */
415
+ export function renderToolSummary(
416
+ summary: ToolCallSummary,
417
+ width: number,
418
+ fg: (color: ThemeColor, text: string) => string,
419
+ ): string {
420
+ const title = fitToolCallSummary(summary, width);
421
+ if (!summary.payload) return fg("toolTitle", title);
422
+ const room = width - visibleWidth(title);
423
+ return room >= MIN_PAYLOAD_WIDTH
424
+ ? `${fg("toolTitle", title)}${fg("dim", headTruncateToWidth(` ${summary.payload}`, room))}`
425
+ : fg("toolTitle", title);
426
+ }