@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
@@ -1,6 +1,6 @@
1
1
  import { normalizeDiffRenderWidth } from "./diff-presentation.ts";
2
- import { pluralize } from "./render-utils.ts";
3
- import { showMoreHintText } from "../../show-more-hint.ts";
2
+ import { pluralize } from "./diff-text.ts";
3
+ import { showMoreHintText } from "../show-more-hint.ts";
4
4
 
5
5
  export interface WidthMeasurementOps {
6
6
  measure(text: string): number;
@@ -94,6 +94,7 @@ export async function executeWriteWithMetadata(
94
94
  await mkdir(dirname(absolutePath), { recursive: true });
95
95
  throwIfAborted();
96
96
  await writeFile(absolutePath, params.content, "utf8");
97
+ // 写入后不再检查 abort:文件已变更,必须如实报告成功
97
98
  store.set(toolCallId, metadata);
98
99
  return {
99
100
  content: [
@@ -7,18 +7,26 @@ import {
7
7
  type Component,
8
8
  } from "@earendil-works/pi-tui";
9
9
  import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
10
- import { sanitizeToolResultText } from "../../utils/tool-result-sanitize.ts";
10
+ import { getToolMouseTui } from "../mouse/scroll.ts";
11
+ import { mcpToolTitle } from "./mcp-title.ts";
12
+ import { collapseHintText, isToolTuiFullscreen, showMoreHintText } from "./show-more-hint.ts";
13
+ import { stripAnsi, stripBackgroundAnsi, stripLeadingStatusIcon } from "../../utils/ansi-text.ts";
14
+ import { walkComponentTree } from "../../utils/component-tree.ts";
11
15
  import {
12
16
  fitToolCallSummary,
13
- pathSummary,
14
- toolViewportWidth,
17
+ humanizeToolLabel,
18
+ renderToolSummary,
19
+ toolCallSummary,
15
20
  type ToolCallSummary,
16
- } from "./result.ts";
17
- import { isToolTuiFullscreen, showMoreHintText } from "../show-more-hint.ts";
21
+ } from "./names.ts";
22
+ import { toolViewportWidth } from "./result.ts";
23
+ import {
24
+ patchRegistry,
25
+ TOOL_GROUPING_GENERATION_KEY as GENERATION_KEY,
26
+ TOOL_GROUPING_PARENT_KEY as PARENT_KEY,
27
+ TOOL_GROUPING_PATCH_KEY as PATCH_KEY,
28
+ } from "../../utils/patch-keys.ts";
18
29
 
19
- const PATCH_KEY = Symbol.for("pi.ccstyle.tool-grouping-patch");
20
- const PARENT_KEY = Symbol.for("pi.ccstyle.tool-grouping-parent");
21
- const GENERATION_KEY = Symbol.for("pi.ccstyle.tool-grouping-generation");
22
30
  const NON_GROUPABLE = new Set(["edit", "write", "apply_patch"]);
23
31
 
24
32
  type Patch = {
@@ -39,7 +47,20 @@ function toolName(tool: any): string {
39
47
  return String(tool?.toolName ?? tool?.toolDefinition?.name ?? "tool");
40
48
  }
41
49
 
42
- function isGroupable(value: unknown): value is any {
50
+ /**
51
+ * 标题:MCP 工具用 adapter 暴露的真实工具名,其余回退工具名人性化。
52
+ */
53
+ function toolTitle(tool: any): string {
54
+ const name = toolName(tool);
55
+ return (
56
+ mcpToolTitle({
57
+ toolName: name,
58
+ definition: tool?.toolDefinition ?? tool?.builtInToolDefinition,
59
+ }) ?? humanizeToolLabel(name)
60
+ );
61
+ }
62
+
63
+ function isGroupable(value: unknown): boolean {
43
64
  return value instanceof ToolExecutionComponent && !NON_GROUPABLE.has(toolName(value));
44
65
  }
45
66
 
@@ -85,37 +106,42 @@ function scheduleGroupAnimation(patch: Patch): void {
85
106
  patch.animationTimer = setTimeout(() => {
86
107
  patch.animationTimer = null;
87
108
  if (!patch.active) return;
109
+ let needsRender = false;
88
110
  for (const group of patch.groups) {
89
111
  if (
90
112
  (group.children as any[]).some(
91
113
  (tool) => tool?.executionStarted && status(tool) === "pending",
92
114
  )
93
- )
115
+ ) {
94
116
  group.invalidate();
117
+ // 分组卡不是 ToolExecutionComponent:它的 invalidate() 不会请求渲染,
118
+ // 不补这一步 spinner 只在别人渲染时才跳帧(并行工具下表现为卡顿/冻结)。
119
+ needsRender = true;
120
+ }
95
121
  }
122
+ if (needsRender) requestAnimationRender(patch);
96
123
  }, TOOL_LOADING_INTERVAL_MS);
97
124
  patch.animationTimer.unref?.();
98
125
  }
99
126
 
100
- function stripAnsi(line: string): string {
101
- return line.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
127
+ /** 借用子工具卡的 ui 请求一帧;子卡缺失时回退到扩展持有的 TUI 槽。 */
128
+ function requestAnimationRender(patch: Patch): void {
129
+ for (const group of patch.groups) {
130
+ const ui = (group.children as any[]).find(
131
+ (tool) => typeof tool?.ui?.requestRender === "function",
132
+ )?.ui;
133
+ if (ui) {
134
+ ui.requestRender();
135
+ return;
136
+ }
137
+ }
138
+ getToolMouseTui()?.requestRender?.();
102
139
  }
103
140
 
104
141
  function visibleLines(lines: string[]): string[] {
105
142
  return lines.filter((line) => stripAnsi(line).trim());
106
143
  }
107
144
 
108
- function stripLeadingStatusIcon(line: string): string {
109
- return line.replace(
110
- /^((?:\x1b\[[0-9;]*m|[ \t]|[├└│─])*)(?:\x1b\[[0-9;]*m)*(?:[✓✗●○■⬤•·⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏])(?:\x1b\[[0-9;]*m)*\s+/,
111
- "$1",
112
- );
113
- }
114
-
115
- export function stripBackgroundAnsi(line: string): string {
116
- return line.replace(/\x1b\[(?:4[0-9]|10[0-7]|48(?:(?:;|:)[0-9]+)+|49)m/g, "");
117
- }
118
-
119
145
  function stripLeadingSpaces(line: string, count: number): string {
120
146
  let offset = 0;
121
147
  let removed = 0;
@@ -138,10 +164,6 @@ function stripLeadingSpaces(line: string, count: number): string {
138
164
  }
139
165
 
140
166
  /** 生成一行铺满 width 的 slot 背景行;bgAnsiOverride 可替换背景 ANSI(用于提亮等)。 */
141
- function panelInnerWidth(width: number): number {
142
- return Math.max(0, Math.floor(width) - 2);
143
- }
144
-
145
167
  export function paddedBackgroundRow(
146
168
  theme: any,
147
169
  slot: string,
@@ -149,12 +171,9 @@ export function paddedBackgroundRow(
149
171
  width: number,
150
172
  bgAnsiOverride?: string,
151
173
  ): string {
152
- const safeWidth = Math.max(0, Math.floor(width));
153
- const leftPadding = safeWidth > 0 ? " " : "";
154
- const rightPadding = safeWidth > 1 ? " " : "";
155
- const innerWidth = panelInnerWidth(width);
174
+ const innerWidth = Math.max(0, width - 2);
156
175
  const clipped = truncateToWidth(stripBackgroundAnsi(content), innerWidth, "");
157
- const row = `${leftPadding}${clipped}${" ".repeat(Math.max(0, innerWidth - visibleWidth(clipped)))}${rightPadding}`;
176
+ const row = ` ${clipped}${" ".repeat(Math.max(0, innerWidth - visibleWidth(clipped)))} `;
158
177
  const bgAnsi =
159
178
  bgAnsiOverride ||
160
179
  (typeof theme?.bg === "function"
@@ -165,113 +184,12 @@ export function paddedBackgroundRow(
165
184
  return `${bgAnsi}${stable}\x1b[49m`;
166
185
  }
167
186
 
168
- function oneLine(value: unknown): string {
169
- return sanitizeToolResultText(String(value ?? ""), 4096)
170
- .replace(/\s+/g, " ")
171
- .trim();
172
- }
173
-
174
- function humanizeToolName(name: string): string {
175
- return name
176
- .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
177
- .replace(/[_-]+/g, " ")
178
- .replace(/\b\w/g, (char) => char.toUpperCase());
179
- }
180
-
181
187
  function toolSummary(tool: any): ToolCallSummary {
182
- const name = toolName(tool);
183
- const lowerName = name.toLowerCase();
184
- const args = tool?.args ?? {};
185
- const titled = humanizeToolName(name);
186
- const value = (fallback: string, ...keys: string[]) => {
187
- const found = keys.map((key) => args[key]).find((item) => typeof item === "string" && item);
188
- return `${titled} ${oneLine(found || fallback)}`;
189
- };
190
- if (lowerName === "agent" || lowerName === "agents") {
191
- const displayName = args.subagent_type ?? args.agent_type ?? args.agent;
192
- if (typeof displayName === "string" && displayName) {
193
- return { main: `${titled} ${displayName}`, detail: "" };
194
- }
195
- return {
196
- main: value(
197
- lowerName === "agent" ? "launch agent" : "launch agents",
198
- "description",
199
- "prompt",
200
- ),
201
- detail: "",
202
- };
203
- }
204
- if (lowerName === "get_subagent_result" || lowerName === "steer_subagent") {
205
- return {
206
- main: value(lowerName === "get_subagent_result" ? "agent result" : "steer agent", "agent_id"),
207
- detail: "",
208
- };
209
- }
210
- if (lowerName === "skill") return { main: value("run skill", "name"), detail: "" };
211
- if (lowerName === "enterplanmode" || lowerName === "enter_plan_mode") {
212
- return { main: `${titled} enable read-only planning`, detail: "" };
213
- }
214
- if (lowerName === "exitplanmode" || lowerName === "exit_plan_mode") {
215
- return { main: `${titled} present plan`, detail: "" };
216
- }
217
- if (lowerName === "taskcreate") return { main: value("create task", "subject"), detail: "" };
218
- if (lowerName === "tasklist") return { main: `${titled} task list`, detail: "" };
219
- if (lowerName === "taskget" || lowerName === "taskupdate") {
220
- return { main: value("task", "taskId", "task_id"), detail: "" };
221
- }
222
- if (lowerName === "taskoutput" || lowerName === "taskstop") {
223
- return { main: value("background task", "task_id", "taskId"), detail: "" };
224
- }
225
- if (lowerName === "taskexecute") {
226
- const ids = Array.isArray(args.task_ids)
227
- ? args.task_ids
228
- : Array.isArray(args.taskIds)
229
- ? args.taskIds
230
- : [];
231
- return {
232
- main: `${titled} ${ids.length ? `${ids[0]}${ids.length > 1 ? ` (+${ids.length - 1} tasks)` : ""}` : "start tasks"}`,
233
- detail: "",
234
- };
235
- }
236
- if (name === "read") {
237
- const details = [
238
- args.offset !== undefined ? `offset=${args.offset}` : "",
239
- args.limit !== undefined ? `limit=${args.limit}` : "",
240
- ].filter(Boolean);
241
- const detail = details.length ? ` (${details.join(", ")})` : "";
242
- if (typeof args.path === "string" && args.path) {
243
- return pathSummary("Read", args.path, tool?.cwd, detail);
244
- }
245
- return { main: "Read ...", detail };
246
- }
247
- if (name === "bash") return { main: `Bash ${oneLine(args.command || "...")}`, detail: "" };
248
- if (name === "grep") {
249
- const pattern = oneLine(args.pattern || "...");
250
- return {
251
- main: `Grep ${JSON.stringify(pattern)}${args.path ? ` in ${oneLine(args.path)}` : ""}`,
252
- detail: "",
253
- };
254
- }
255
- if (name === "find") {
256
- const pattern = oneLine(args.pattern || "...");
257
- return {
258
- main: `Find ${JSON.stringify(pattern)}${args.path ? ` in ${oneLine(args.path)}` : ""}`,
259
- detail: "",
260
- };
261
- }
262
- const preferred =
263
- args.agent_id ??
264
- args.path ??
265
- args.file_path ??
266
- args.url ??
267
- args.description ??
268
- args.query ??
269
- args.name ??
270
- args.prompt;
271
- return {
272
- main: `${humanizeToolName(name)}${preferred === undefined ? "" : ` ${oneLine(preferred)}`}`,
273
- detail: "",
274
- };
188
+ return toolCallSummary(toolName(tool), tool?.args ?? {}, {
189
+ title: toolTitle(tool),
190
+ variant: "grouping",
191
+ cwd: tool?.cwd,
192
+ });
275
193
  }
276
194
 
277
195
  function toolNameList(tools: any[]): string {
@@ -282,10 +200,6 @@ function toolNameList(tools: any[]): string {
282
200
 
283
201
  let nextGroupId = 1;
284
202
 
285
- function groupChildWidth(width: number): number {
286
- return Math.max(1, panelInnerWidth(width));
287
- }
288
-
289
203
  type SettledGroupCache = {
290
204
  width: number;
291
205
  hover: boolean;
@@ -297,6 +211,15 @@ type SettledGroupCache = {
297
211
  lines: string[];
298
212
  };
299
213
 
214
+ type ExpandedGroupCache = {
215
+ width: number;
216
+ hover: boolean;
217
+ theme: unknown;
218
+ fullscreen: boolean;
219
+ paints: readonly unknown[];
220
+ lines: string[];
221
+ };
222
+
300
223
  export class ToolGroupComponent extends Container {
301
224
  readonly toolCallId = `ccstyle-tool-group-${nextGroupId++}`;
302
225
  readonly toolName = "Tool group";
@@ -307,8 +230,10 @@ export class ToolGroupComponent extends Container {
307
230
  }
308
231
  private hintHovered = false;
309
232
  private readonly patch: Patch;
310
- /** 仅缓存已完成且折叠的分组;pending / expanded 每帧现算。 */
233
+ /** 仅缓存已完成且折叠的分组;pending 每帧现算。 */
311
234
  private settledCache: SettledGroupCache | undefined;
235
+ /** 展开分组:子工具 paint 引用未变则复用整卡行。 */
236
+ private expandedPaintCache: ExpandedGroupCache | undefined;
312
237
 
313
238
  constructor(patch: Patch) {
314
239
  super();
@@ -316,14 +241,19 @@ export class ToolGroupComponent extends Container {
316
241
  patch.groups.add(this);
317
242
  }
318
243
 
319
- addTool(tool: any): void {
244
+ private clearPaintCache(): void {
320
245
  this.settledCache = undefined;
246
+ this.expandedPaintCache = undefined;
247
+ }
248
+
249
+ addTool(tool: any): void {
250
+ this.clearPaintCache();
321
251
  this.children.push(tool);
322
252
  tool[PARENT_KEY] = this;
323
253
  }
324
254
 
325
255
  releaseTools(): any[] {
326
- this.settledCache = undefined;
256
+ this.clearPaintCache();
327
257
  const tools = [...this.children];
328
258
  this.children.length = 0;
329
259
  this.patch.groups.delete(this);
@@ -331,27 +261,27 @@ export class ToolGroupComponent extends Container {
331
261
  }
332
262
 
333
263
  removeTool(tool: any): void {
334
- this.settledCache = undefined;
264
+ this.clearPaintCache();
335
265
  const index = this.children.indexOf(tool);
336
266
  if (index >= 0) this.children.splice(index, 1);
337
267
  if (tool?.[PARENT_KEY] === this) delete tool[PARENT_KEY];
338
268
  }
339
269
 
340
270
  setExpanded(expanded: boolean): void {
341
- if (this._expanded !== expanded) this.settledCache = undefined;
271
+ if (this._expanded !== expanded) this.clearPaintCache();
342
272
  this._expanded = expanded;
343
273
  for (const tool of this.children)
344
274
  (tool as Component & { setExpanded?: (expanded: boolean) => void }).setExpanded?.(expanded);
345
275
  }
346
276
 
347
277
  setHintHovered(hovered: boolean): void {
348
- if (this.hintHovered !== hovered) this.settledCache = undefined;
278
+ if (this.hintHovered !== hovered) this.clearPaintCache();
349
279
  this.hintHovered = hovered;
350
280
  }
351
281
 
352
282
  /**
353
283
  * 展开时按局部行定位内部组件(null = 行属于 group 自身:空行/头行/尾行)。
354
- * 行数计算与 render 保持一致:预留背景 padding、分支和状态前缀后再过滤空行。
284
+ * 行数计算与 render 保持一致:宽度 width-2 + 空行过滤。
355
285
  */
356
286
  childAtRow(localRow: number, width: number): { component: any; row: number } | null {
357
287
  if (!this._expanded || localRow < 2) return null;
@@ -359,7 +289,7 @@ export class ToolGroupComponent extends Container {
359
289
  for (const tool of this.children) {
360
290
  let lines: string[] = [];
361
291
  try {
362
- const rendered = tool.render?.(groupChildWidth(width));
292
+ const rendered = tool.render?.(Math.max(1, width - 2));
363
293
  if (Array.isArray(rendered)) lines = visibleLines(rendered.map((line) => String(line)));
364
294
  } catch {
365
295
  lines = [];
@@ -374,7 +304,7 @@ export class ToolGroupComponent extends Container {
374
304
  }
375
305
 
376
306
  invalidate(): void {
377
- this.settledCache = undefined;
307
+ this.clearPaintCache();
378
308
  for (const tool of this.children) tool.invalidate?.();
379
309
  }
380
310
 
@@ -434,8 +364,7 @@ export class ToolGroupComponent extends Container {
434
364
  })
435
365
  .join(` ${fg("dim", "•")} `);
436
366
  const names = new Set(this.children.map(toolName));
437
- const label =
438
- names.size === 1 ? humanizeToolName(toolName(this.children[0])) : "Multiple Tools";
367
+ const label = names.size === 1 ? toolTitle(this.children[0]) : "Multiple Tools";
439
368
  const overall: ToolStatus = counts.error ? "error" : counts.pending ? "pending" : "success";
440
369
  if (
441
370
  (this.children as any[]).some((tool) => tool?.executionStarted && status(tool) === "pending")
@@ -444,16 +373,42 @@ export class ToolGroupComponent extends Container {
444
373
  const overallColor = overall === "pending" ? "accent" : overall;
445
374
  const nameList = names.size > 1 ? ` ${fg("dim", `• ${toolNameList(this.children)}`)}` : "";
446
375
  // 圆点保持 dim;hover 只高亮可点击文字。
447
- const hint = `${fg("dim", "•")} ${fg(this.hintHovered ? "text" : "dim", showMoreHintText())}`;
448
- const lines = [
449
- "",
450
- truncateToWidth(
451
- ` ${fg(overallColor, "●")} ${label}: ${countText}${nameList} ${hint}`,
452
- toolViewportWidth(width),
453
- "…",
454
- ),
455
- ];
376
+ const hintText = this._expanded ? collapseHintText() : showMoreHintText();
377
+ const hint = `${fg("dim", "•")} ${fg(this.hintHovered ? "text" : "dim", hintText)}`;
378
+ // 行尾提示承载折叠分组的可点击区(collapsedHintHitbox 依赖行尾短语)。
379
+ // 空间不足时先省略工具名列表,再截断计数尾部,提示最后让位;
380
+ // 极窄视口连“圆点+标题+计数”都放不下时才回退整行截断。
381
+ const viewport = toolViewportWidth(width);
382
+ const head = ` ${fg(overallColor, "●")} ${label}: ${countText}`;
383
+ const hintWidth = visibleWidth(` ${hint}`);
384
+ const nameBudget = viewport - visibleWidth(head) - hintWidth;
385
+ const elidedName =
386
+ nameList && nameBudget < visibleWidth(nameList)
387
+ ? nameBudget >= 4
388
+ ? ` ${fg("dim", "…")}`
389
+ : ""
390
+ : nameList;
391
+ const headBudget = viewport - hintWidth - visibleWidth(elidedName);
392
+ const titleLine = `${visibleWidth(head) <= headBudget ? head : truncateToWidth(head, headBudget, "…")}${elidedName} ${hint}`;
393
+ const lines = ["", truncateToWidth(titleLine, viewport, "…")];
456
394
  const total = this.children.length;
395
+ const childPaints = this._expanded
396
+ ? (this.children as any[]).map((tool) => tool.render?.(Math.max(1, width - 2)))
397
+ : undefined;
398
+ if (this._expanded && childPaints) {
399
+ const expandedHit = this.expandedPaintCache;
400
+ if (
401
+ expandedHit &&
402
+ expandedHit.width === width &&
403
+ expandedHit.hover === this.hintHovered &&
404
+ expandedHit.theme === this.patch.theme &&
405
+ expandedHit.fullscreen === isToolTuiFullscreen() &&
406
+ expandedHit.paints.length === childPaints.length &&
407
+ expandedHit.paints.every((paint, index) => paint === childPaints[index])
408
+ ) {
409
+ return expandedHit.lines;
410
+ }
411
+ }
457
412
  const expandedLines: string[] = [];
458
413
  for (let index = 0; index < total; index++) {
459
414
  const tool = this.children[index];
@@ -465,17 +420,19 @@ export class ToolGroupComponent extends Container {
465
420
  const summary = toolSummary(tool);
466
421
  const prefix = ` ${fg("dim", branch)} ${fg(color, statusIcon(toolStatus))} `;
467
422
  const detail = fg("dim", summary.detail);
468
- const mainWidth = Math.max(0, width - visibleWidth(prefix) - visibleWidth(detail));
423
+ // 与单工具卡标题同宽,宽屏右侧留白一致
424
+ const rowWidth = toolViewportWidth(width);
425
+ const mainWidth = Math.max(0, rowWidth - visibleWidth(prefix) - visibleWidth(detail));
469
426
  lines.push(
470
427
  truncateToWidth(
471
- `${prefix}${fg("toolTitle", fitToolCallSummary(summary, mainWidth))}${detail}`,
472
- width,
428
+ `${prefix}${renderToolSummary(summary, mainWidth, fg)}${detail}`,
429
+ rowWidth,
473
430
  "",
474
431
  ),
475
432
  );
476
433
  continue;
477
434
  }
478
- const rendered = visibleLines(tool.render(groupChildWidth(width)));
435
+ const rendered = visibleLines(Array.isArray(childPaints?.[index]) ? childPaints[index] : []);
479
436
  if (rendered.length) {
480
437
  rendered[0] = stripLeadingStatusIcon(rendered[0])
481
438
  .replace(/^ +/, "")
@@ -484,7 +441,8 @@ export class ToolGroupComponent extends Container {
484
441
  const childLines = rendered.length ? rendered : [toolSummary(tool).main];
485
442
  for (let lineIndex = 0; lineIndex < childLines.length; lineIndex++) {
486
443
  const content =
487
- lineIndex === 0 ? childLines[lineIndex] : stripLeadingSpaces(childLines[lineIndex], 2);
444
+ // 续行只剥外层 Box 的 1 格 left pad,保留 Input/Output 相对缩进
445
+ lineIndex === 0 ? childLines[lineIndex] : stripLeadingSpaces(childLines[lineIndex], 1);
488
446
  const prefix =
489
447
  lineIndex === 0
490
448
  ? `${fg("dim", branch)} ${fg(color, statusIcon(toolStatus))} `
@@ -499,6 +457,14 @@ export class ToolGroupComponent extends Container {
499
457
  lines.push(paddedBackgroundRow(theme, backgroundSlot, line, width));
500
458
  }
501
459
  lines.push(paddedBackgroundRow(theme, backgroundSlot, "", width));
460
+ this.expandedPaintCache = {
461
+ width,
462
+ hover: this.hintHovered,
463
+ theme: this.patch.theme,
464
+ fullscreen: isToolTuiFullscreen(),
465
+ paints: childPaints ?? [],
466
+ lines,
467
+ };
502
468
  } else if (counts.pending === 0) {
503
469
  this.storeSettledCache(width, lines);
504
470
  }
@@ -525,7 +491,7 @@ function ungroup(patch: Patch): void {
525
491
  }
526
492
  }
527
493
 
528
- function normalizeGroup(group: ToolGroupComponent): void {
494
+ function normalizeGroup(patch: Patch, group: ToolGroupComponent): void {
529
495
  if (group.children.length > 1) return;
530
496
  const parent = (group as any)[PARENT_KEY];
531
497
  const index = parent?.children?.indexOf(group) ?? -1;
@@ -574,32 +540,17 @@ function maybeGroup(patch: Patch, parent: any, component: any): void {
574
540
  /** /reload 不会重新 addChild;扫描当前 mounted roots,把已有工具重新送入同一分组规则。 */
575
541
  function regroup(patch: Patch, root: any): void {
576
542
  if (!patch.active || !patch.enabled() || !root) return;
577
- const seen = new Set<any>();
578
- const visit = (value: any): void => {
579
- if (!value || typeof value !== "object" || seen.has(value)) return;
580
- seen.add(value);
581
- if (Array.isArray(value)) {
582
- for (const child of value) visit(child);
583
- return;
584
- }
543
+ walkComponentTree(root, (value: any) => {
544
+ // 分组卡与可分组工具是分组边界:不继续下钻(与原有遍历过滤一致)。
545
+ if (value instanceof ToolGroupComponent || isGroupable(value)) return false;
585
546
  const children = value.children;
586
547
  if (Array.isArray(children)) {
587
548
  for (const child of [...children]) {
588
549
  if (child && typeof child === "object") child[PARENT_KEY] = value;
589
550
  maybeGroup(patch, value, child);
590
551
  }
591
- for (const child of [...children]) {
592
- if (!(child instanceof ToolGroupComponent) && !isGroupable(child)) visit(child);
593
- }
594
552
  }
595
- try {
596
- const mounted = value.getMountedRoots?.();
597
- if (Array.isArray(mounted)) visit(mounted);
598
- } catch {
599
- // renderer 切换中的惰性 Proxy 可能暂时没有 mounted roots。
600
- }
601
- };
602
- visit(root);
553
+ });
603
554
  }
604
555
 
605
556
  export type ToolGroupingHooks = {
@@ -610,8 +561,7 @@ export type ToolGroupingHooks = {
610
561
 
611
562
  export function installToolGrouping(getEnabled: () => boolean): ToolGroupingHooks {
612
563
  const prototype = Container.prototype as any;
613
- const host = globalThis as any;
614
- const previous = host[PATCH_KEY] as Patch | undefined;
564
+ const previous = patchRegistry.get<Patch>(PATCH_KEY);
615
565
  if (previous) {
616
566
  previous.active = false;
617
567
  previous.enabled = () => false;
@@ -656,12 +606,12 @@ export function installToolGrouping(getEnabled: () => boolean): ToolGroupingHook
656
606
  const group = component?.[PARENT_KEY];
657
607
  if (group instanceof ToolGroupComponent && (group as any)[PARENT_KEY] === this) {
658
608
  group.removeTool(component);
659
- normalizeGroup(group);
609
+ normalizeGroup(patch, group);
660
610
  return;
661
611
  }
662
612
  const result = patch.original.removeChild.call(this, component);
663
613
  if (component?.[PARENT_KEY] === this) delete component[PARENT_KEY];
664
- if (this instanceof ToolGroupComponent) normalizeGroup(this);
614
+ if (this instanceof ToolGroupComponent) normalizeGroup(patch, this);
665
615
  if (component instanceof ToolGroupComponent) {
666
616
  for (const tool of component.releaseTools()) delete tool[PARENT_KEY];
667
617
  }
@@ -681,7 +631,7 @@ export function installToolGrouping(getEnabled: () => boolean): ToolGroupingHook
681
631
  prototype.addChild = patch.installed.addChild;
682
632
  prototype.removeChild = patch.installed.removeChild;
683
633
  prototype.clear = patch.installed.clear;
684
- host[PATCH_KEY] = patch;
634
+ patchRegistry.install(PATCH_KEY, patch);
685
635
  return {
686
636
  setTheme(theme: any) {
687
637
  patch.theme = theme;
@@ -0,0 +1,39 @@
1
+ import { humanizeToolLabel } from "./names.ts";
2
+
3
+ /**
4
+ * MCP 工具的识别与标题,单卡(default-mode)与分组行/分组头(grouping)共用。
5
+ *
6
+ * - 判定:工具名带 mcp 片段(`mcp` 网关、`mcpScript`、`mcp__<server>`),或 adapter 注册时
7
+ * 给的 label 以 `MCP:` 开头。默认/none/short 前缀模式下直连工具的名字里没有 mcp 片段
8
+ * (`github_search_code`、`search_code`),只能靠 label 判定。
9
+ * - 标题:具体工具用 adapter 暴露的真实工具名,不做二次格式化;只有 `mcp` / `mcpScript`
10
+ * 这两个入口没有具体工具名,跟普通工具一样人性化(MCP / MCP Script)。
11
+ * - 参数摘要与其它工具共用 toolCallSummary 的字段链,网关额外把 `args.tool` 提到标题后。
12
+ */
13
+
14
+ /** 单次 MCP 调用的识别信息。 */
15
+ export type McpToolCall = {
16
+ toolName: string;
17
+ /** 工具定义;默认/none/short 前缀模式要靠 label 判定,会话重放时可能缺失。 */
18
+ definition?: any;
19
+ };
20
+
21
+ const GATEWAY_TOOL_NAME = "mcp";
22
+ const SCRIPT_TOOL_NAME = "mcpScript";
23
+
24
+ export function isMcpToolDefinition(definition: any, toolName: string): boolean {
25
+ const label = typeof definition?.label === "string" ? definition.label.trim() : "";
26
+ if (/^MCP(?::|$)/i.test(label)) return true;
27
+ if (toolName === GATEWAY_TOOL_NAME || toolName === SCRIPT_TOOL_NAME) return true;
28
+ return /^mcp[_:-]|[_:-]mcp[_:-]/i.test(toolName);
29
+ }
30
+
31
+ /** 标题:MCP 工具返回标题(入口走人性化,其余用真实工具名);非 MCP 返回 undefined。 */
32
+ export function mcpToolTitle(call: McpToolCall): string | undefined {
33
+ const { toolName } = call;
34
+ if (!isMcpToolDefinition(call.definition, toolName)) return undefined;
35
+ if (toolName === GATEWAY_TOOL_NAME || toolName === SCRIPT_TOOL_NAME) {
36
+ return humanizeToolLabel(toolName);
37
+ }
38
+ return toolName;
39
+ }