@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,32 +1,38 @@
1
1
  import { Text, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
2
- import { posix, win32 } from "node:path";
3
2
  import { inspect } from "node:util";
4
3
  import { config } from "../../config/config.ts";
5
- import { showMoreHintText } from "../show-more-hint.ts";
4
+ import { showMoreHintText } from "./show-more-hint.ts";
6
5
  import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
6
+ import { getToolMouseTui } from "../mouse/scroll.ts";
7
7
  import { sanitizeToolResultText } from "../../utils/tool-result-sanitize.ts";
8
8
 
9
+ const TOOL_VIEWPORT_WIDTH_RATIO = 0.8;
10
+ /** 宽屏右侧留白上限:比例留白超过这么多列时改用固定留白。 */
11
+ const TOOL_VIEWPORT_MAX_GUTTER = 24;
12
+
9
13
  export function toolViewportWidth(width: number): number {
10
- return Math.max(1, Math.floor(width));
14
+ return Math.max(
15
+ 1,
16
+ Math.floor(width * TOOL_VIEWPORT_WIDTH_RATIO),
17
+ Math.floor(width) - TOOL_VIEWPORT_MAX_GUTTER,
18
+ );
11
19
  }
12
20
 
13
- /** 与默认工具结果相同的一级缩进包装。 */
21
+ /** 与默认工具结果相同的一级缩进包装。子组件只扣始终加上的 1 列;↳ 行多出的 2 列由 truncateToWidth 吃掉。 */
14
22
  export function insetComponent(component: any): any {
15
- return {
23
+ const wrapped: Record<string, unknown> = {
16
24
  render: (width: number) =>
17
25
  component.render(Math.max(1, width - 1)).map((line: string) => {
18
26
  const nestedMarker = line.replace(/^((?:\x1b\[[0-?]*[ -/]*[@-~])*)↳/, "$1 ↳");
19
- return ` ${nestedMarker}`;
27
+ return truncateToWidth(` ${nestedMarker}`, Math.max(0, width), "");
20
28
  }),
21
29
  invalidate: () => component.invalidate?.(),
22
30
  };
23
- }
24
-
25
- export function oneLine(value: unknown, max = 4096): string {
26
- const text = sanitizeToolResultText(String(value ?? ""))
27
- .replace(/\s+/g, " ")
28
- .trim();
29
- return text.length > max ? `${text.slice(0, max - 1)}…` : text;
31
+ // 内层 diff 声明的 remainder 行:鼠标层只能看到这个包装组件。
32
+ if (typeof component.isCollapsedHintLine === "function") {
33
+ wrapped.isCollapsedHintLine = (line: string) => component.isCollapsedHintLine(line);
34
+ }
35
+ return wrapped;
30
36
  }
31
37
 
32
38
  function rawTextFromResult(result: any): string {
@@ -72,14 +78,11 @@ function hasExpandableResult(text: string): boolean {
72
78
  return countLines(text) > 1;
73
79
  }
74
80
 
75
- function toolIcon(_name: string): string {
76
- return "●";
77
- }
78
-
79
81
  const activeAnimationContexts = new Set<any>();
80
82
  let sharedAnimationTimer: ReturnType<typeof setTimeout> | null = null;
81
83
 
82
84
  function clearAnimation(context: any) {
85
+ if (context?.state) context.state.ccstyleAnimationLight = false;
83
86
  if (!context?.state?.ccstyleAnimationScheduled) return;
84
87
  context.state.ccstyleAnimationScheduled = false;
85
88
  activeAnimationContexts.delete(context);
@@ -92,6 +95,7 @@ function clearAnimation(context: any) {
92
95
  export function clearAllAnimations() {
93
96
  for (const ctx of activeAnimationContexts) {
94
97
  ctx.state.ccstyleAnimationScheduled = false;
98
+ ctx.state.ccstyleAnimationLight = false;
95
99
  }
96
100
  activeAnimationContexts.clear();
97
101
  if (sharedAnimationTimer) {
@@ -100,8 +104,15 @@ export function clearAllAnimations() {
100
104
  }
101
105
  }
102
106
 
103
- export function scheduleAnimation(context: any, intervalMs = TOOL_LOADING_INTERVAL_MS) {
107
+ export function scheduleAnimation(
108
+ context: any,
109
+ options: { light?: boolean; intervalMs?: number } = {},
110
+ ) {
104
111
  const state = (context.state ??= {});
112
+ // light:调用方自己在 render() 内重取 loading 帧,定时器只需请求重绘;
113
+ // 否则定时器走 context.invalidate()(compact 摘要等靠它重建)。
114
+ // 每次调用都按本次 options 写入,避免上次 light 残留后非 light 调用仍跳过 invalidate。
115
+ state.ccstyleAnimationLight = Boolean(options.light);
105
116
  if (state.ccstyleAnimationScheduled) return;
106
117
  state.ccstyleAnimationScheduled = true;
107
118
  activeAnimationContexts.add(context);
@@ -110,11 +121,16 @@ export function scheduleAnimation(context: any, intervalMs = TOOL_LOADING_INTERV
110
121
  sharedAnimationTimer = null;
111
122
  const contexts = Array.from(activeAnimationContexts);
112
123
  activeAnimationContexts.clear();
124
+ const tui = getToolMouseTui();
125
+ const canRequestRender = typeof tui?.requestRender === "function";
126
+ let lightFrames = false;
113
127
  for (const ctx of contexts) {
114
128
  ctx.state.ccstyleAnimationScheduled = false;
115
- ctx.invalidate?.();
129
+ if (ctx.state.ccstyleAnimationLight && canRequestRender) lightFrames = true;
130
+ else ctx.invalidate?.();
116
131
  }
117
- }, intervalMs);
132
+ if (lightFrames) tui.requestRender();
133
+ }, options.intervalMs ?? TOOL_LOADING_INTERVAL_MS);
118
134
  }
119
135
  }
120
136
 
@@ -127,7 +143,8 @@ type ToolVisualState = "pending" | "success" | "error";
127
143
  export function settledIcon(name: string, state: ToolVisualState | undefined): string {
128
144
  if (state === "success") return "✓";
129
145
  if (state === "error") return "✗";
130
- return toolIcon(name);
146
+ // toolIcon(name) 曾恒返回 "●",已内联;name 保留以稳定导出签名。
147
+ return "●";
131
148
  }
132
149
 
133
150
  export function setToolVisualState(context: any, visualState: ToolVisualState) {
@@ -213,7 +230,7 @@ export class ExpandedToolResultText {
213
230
  }
214
231
  }
215
232
 
216
- /** Affordance next to truncated Input/Output headers — click opens full preview. */
233
+ /** 截断体末行 `… +N more lines` 旁的展开提示,点击打开全量预览。 */
217
234
  export const SHOW_MORE_LABEL = "• click to show more";
218
235
 
219
236
  export type ToolIoSection = "input" | "output";
@@ -232,6 +249,8 @@ const EXPANDED_TOOL_IO_VIEW_GENERATION = Symbol("ccstyle-expanded-tool-io-view")
232
249
  * └ Output click to show more
233
250
  * result line…
234
251
  *
252
+ * flushLeft=true(仅 mode=on 展开卡):去掉树线前导空格,由外层 Box(1,1) 提供 1 格 padding。
253
+ *
235
254
  * Reused across re-renders via context.lastComponent when possible.
236
255
  */
237
256
  export class ExpandedToolIoView {
@@ -247,16 +266,20 @@ export class ExpandedToolIoView {
247
266
  private hoveredSection: ToolIoSection | null = null;
248
267
  /** Which sections currently show the show-more affordance (after last render). */
249
268
  private truncated: { input: boolean; output: boolean } = { input: false, output: false };
250
- /** 0-based body/footer line indexes that carry show-more after last render. */
269
+ /** 0-based header line indexes that carry show-more after last render. */
251
270
  private showMoreHeaderRows: { input?: number; output?: number } = {};
252
271
 
272
+ /** flushLeft:贴左渲染(mode=on 展开卡);默认 false 保留前导空格(compact 等共用路径)。 */
273
+ private flushLeft: boolean;
274
+
253
275
  constructor(
254
276
  theme: any,
255
277
  inputBody: string,
256
278
  outputBody: string,
257
279
  isError: boolean,
258
- maxOutputLines = config.expandedPreviewMaxLines,
259
- maxInputLines = config.expandedPreviewMaxLines,
280
+ maxOutputLines = config.expandedOutputMaxLines,
281
+ maxInputLines = config.expandedInputMaxLines,
282
+ flushLeft = false,
260
283
  ) {
261
284
  this.theme = theme;
262
285
  this.inputBody = inputBody;
@@ -264,6 +287,7 @@ export class ExpandedToolIoView {
264
287
  this.isError = isError;
265
288
  this.maxOutputLines = Math.max(1, maxOutputLines);
266
289
  this.maxInputLines = Math.max(1, maxInputLines);
290
+ this.flushLeft = flushLeft;
267
291
  }
268
292
 
269
293
  setContent(
@@ -272,16 +296,19 @@ export class ExpandedToolIoView {
272
296
  isError: boolean,
273
297
  maxOutputLines?: number,
274
298
  maxInputLines?: number,
299
+ flushLeft?: boolean,
275
300
  ): void {
276
301
  const nextOut =
277
302
  maxOutputLines !== undefined ? Math.max(1, maxOutputLines) : this.maxOutputLines;
278
303
  const nextIn = maxInputLines !== undefined ? Math.max(1, maxInputLines) : this.maxInputLines;
304
+ const nextFlush = flushLeft !== undefined ? flushLeft : this.flushLeft;
279
305
  if (
280
306
  this.inputBody === inputBody &&
281
307
  this.outputBody === outputBody &&
282
308
  this.isError === isError &&
283
309
  this.maxOutputLines === nextOut &&
284
- this.maxInputLines === nextIn
310
+ this.maxInputLines === nextIn &&
311
+ this.flushLeft === nextFlush
285
312
  ) {
286
313
  return;
287
314
  }
@@ -290,6 +317,7 @@ export class ExpandedToolIoView {
290
317
  this.isError = isError;
291
318
  this.maxOutputLines = nextOut;
292
319
  this.maxInputLines = nextIn;
320
+ this.flushLeft = nextFlush;
293
321
  this.invalidate();
294
322
  }
295
323
 
@@ -307,7 +335,11 @@ export class ExpandedToolIoView {
307
335
  this.invalidate();
308
336
  }
309
337
 
310
- /** True when the plain footer line is a truncated section with show-more. */
338
+ getHoveredSection(): ToolIoSection | null {
339
+ return this.hoveredSection;
340
+ }
341
+
342
+ /** True when the plain truncation footer carries show-more. Input 续行带 │,Output 不带。 */
311
343
  matchShowMoreLine(plainLine: string): ToolIoSection | null {
312
344
  const line = plainLine.replace(/\x1b\[[0-9;]*m/g, "");
313
345
  if (!line.includes(` • ${showMoreHintText()}`) || !/\+\d+ more lines/.test(line)) return null;
@@ -347,7 +379,9 @@ export class ExpandedToolIoView {
347
379
 
348
380
  const theme = this.theme;
349
381
  const safeWidth = Math.max(1, Math.floor(width));
350
- const rail = " │ ";
382
+ // flushLeft:贴左(外层 Box 负责 1 格 pad);否则保留 1 格前导空格(compact 共用)
383
+ const lead = this.flushLeft ? "" : " ";
384
+ const rail = `${lead}│ `;
351
385
  const railWidth = visibleWidth(rail);
352
386
  const bodyWidth = toolViewportWidth(safeWidth);
353
387
  const contentWidth = Math.max(1, bodyWidth - railWidth);
@@ -357,7 +391,7 @@ export class ExpandedToolIoView {
357
391
  this.showMoreHeaderRows = {};
358
392
 
359
393
  const pushHeader = (corner: "├" | "└", label: string) => {
360
- const mark = theme.fg("dim", ` ${corner} `);
394
+ const mark = theme.fg("dim", `${lead}${corner} `);
361
395
  const title = theme.fg(
362
396
  "accent",
363
397
  typeof theme.bold === "function" ? theme.bold(label) : label,
@@ -366,12 +400,13 @@ export class ExpandedToolIoView {
366
400
  };
367
401
 
368
402
  const pushRailLine = (styledContent: string, continued = true) => {
369
- const prefix = continued ? rail : " ";
403
+ // 续行 rail;Output 正文相对 └ 缩进 2 格(+ 可选 lead)
404
+ const prefix = continued ? rail : `${lead} `;
370
405
  lines.push(truncateToWidth(theme.fg("dim", prefix) + styledContent, safeWidth, ""));
371
406
  };
372
407
 
373
408
  const pushBlankRail = () => {
374
- lines.push(truncateToWidth(theme.fg("dim", " │"), safeWidth, ""));
409
+ lines.push(truncateToWidth(theme.fg("dim", `${lead}│`), safeWidth, ""));
375
410
  };
376
411
 
377
412
  /** Style `key: value` input rows — dim keys, readable values. */
@@ -407,6 +442,7 @@ export class ExpandedToolIoView {
407
442
  if (truncated) {
408
443
  const hidden = Math.max(0, wrapped.length - visible.length);
409
444
  if (hidden > 0) {
445
+ // hover 只高亮文字,圆点保持 dim(与 group hint 一致)。
410
446
  const more =
411
447
  theme.fg("dim", " •") +
412
448
  theme.fg(
@@ -423,6 +459,7 @@ export class ExpandedToolIoView {
423
459
  const hasInput = this.inputBody.trim().length > 0;
424
460
  const outputText = this.getOutputBody();
425
461
 
462
+ // Decide show-more from the same truncation rules as pushBody.
426
463
  const inputWouldTruncate =
427
464
  hasInput &&
428
465
  bodyExceedsLineLimit(this.inputBody, this.maxInputLines, contentWidth, true, theme);
@@ -447,11 +484,19 @@ export class ExpandedToolIoView {
447
484
  pushBlankRail();
448
485
  this.truncated.output = outputWouldTruncate;
449
486
  pushHeader("└", "Output");
450
- pushBody(outputText, { limit: this.maxOutputLines, continued: false, section: "output" });
487
+ pushBody(outputText, {
488
+ limit: this.maxOutputLines,
489
+ continued: false,
490
+ section: "output",
491
+ });
451
492
  } else {
452
493
  this.truncated.output = outputWouldTruncate;
453
494
  pushHeader("└", "Output");
454
- pushBody(outputText, { limit: this.maxOutputLines, continued: false, section: "output" });
495
+ pushBody(outputText, {
496
+ limit: this.maxOutputLines,
497
+ continued: false,
498
+ section: "output",
499
+ });
455
500
  }
456
501
 
457
502
  this.cachedWidth = width;
@@ -558,103 +603,6 @@ export function middleTruncateToWidth(text: string, width: number): string {
558
603
  return `${left}…${right}`;
559
604
  }
560
605
 
561
- export type ToolCallSummary = {
562
- main: string;
563
- detail: string;
564
- /** 路径摘要保留结构,供最终渲染按实际宽度优先保留文件名。 */
565
- path?: { prefix: string; value: string };
566
- };
567
-
568
- function pathApi(value: string) {
569
- if (posix.isAbsolute(value)) return posix;
570
- if (win32.isAbsolute(value)) return win32;
571
- return undefined;
572
- }
573
-
574
- /** cwd 内绝对路径转相对路径;cwd 外路径保持不变。 */
575
- export function displayPath(value: unknown, cwd?: string): string {
576
- const text = oneLine(value, 4096);
577
- const base = cwd ? oneLine(cwd, 4096) : "";
578
- const api = pathApi(text);
579
- if (!api || !base || !api.isAbsolute(base)) return text;
580
- const relative = api.relative(base, text);
581
- if (!relative) return api.basename(text) || ".";
582
- if (relative === ".." || relative.startsWith(`..${api.sep}`) || api.isAbsolute(relative)) {
583
- return text;
584
- }
585
- return relative;
586
- }
587
-
588
- function headToWidth(text: string, width: number, ellipsis = ""): string {
589
- if (visibleWidth(text) <= width) return text;
590
- if (width <= 0) return "";
591
- const suffix = visibleWidth(ellipsis) <= width ? ellipsis : "";
592
- const contentWidth = width - visibleWidth(suffix);
593
- let head = "";
594
- for (const char of Array.from(text)) {
595
- if (visibleWidth(head + char) > contentWidth) break;
596
- head += char;
597
- }
598
- return head + suffix;
599
- }
600
-
601
- function tailToWidth(text: string, width: number): string {
602
- if (width <= 0) return "";
603
- let tail = "";
604
- for (const char of Array.from(text).reverse()) {
605
- if (visibleWidth(char + tail) > width) break;
606
- tail = char + tail;
607
- }
608
- return tail;
609
- }
610
-
611
- /** 中间截断路径:目录保留开头,末尾优先完整保留文件名。 */
612
- export function truncatePathToWidth(path: string, width: number): string {
613
- if (visibleWidth(path) <= width) return path;
614
- if (width <= 1) return width === 1 ? "…" : "";
615
- const separatorIndex = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
616
- const filename = separatorIndex >= 0 ? path.slice(separatorIndex + 1) : path;
617
- const filenameWidth = visibleWidth(filename);
618
- if (separatorIndex >= 0 && filenameWidth + 1 <= width) {
619
- const separator = path[separatorIndex]!;
620
- const suffix = `${separator}${filename}`;
621
- const prefixWidth = width - visibleWidth(suffix) - 1;
622
- const prefix = headToWidth(path.slice(0, separatorIndex), Math.max(0, prefixWidth));
623
- return `${prefix}…${prefix ? suffix : filename}`;
624
- }
625
- const leftWidth = Math.max(1, Math.floor((width - 1) * 0.45));
626
- const rightWidth = Math.max(0, width - leftWidth - 1);
627
- return `${headToWidth(filename, leftWidth)}…${tailToWidth(filename, rightWidth)}`;
628
- }
629
-
630
- /** 路径展示的统一入口:相对化后按当前可用宽度中间截断。 */
631
- export function formatDisplayPath(value: unknown, cwd: string | undefined, width: number): string {
632
- return truncatePathToWidth(displayPath(value, cwd), Math.max(0, Math.floor(width)));
633
- }
634
-
635
- /** 按最终终端宽度渲染摘要;路径摘要不会再被整行头部截断。 */
636
- export function fitToolCallSummary(summary: ToolCallSummary, width: number): string {
637
- if (!summary.path) return headTruncateToWidth(summary.main, width);
638
- const prefix = summary.path.prefix;
639
- const pathWidth = Math.max(0, width - visibleWidth(prefix) - 1);
640
- if (pathWidth <= 0) return headTruncateToWidth(prefix, width);
641
- return `${prefix} ${truncatePathToWidth(summary.path.value, pathWidth)}`;
642
- }
643
-
644
- export function pathSummary(
645
- prefix: string,
646
- value: unknown,
647
- cwd: string | undefined,
648
- detail = "",
649
- ): ToolCallSummary {
650
- const path = displayPath(value, cwd);
651
- return {
652
- main: `${prefix} ${path}`,
653
- detail,
654
- path: { prefix, value: path },
655
- };
656
- }
657
-
658
606
  /** Pretty-print tool call args for the expanded Input section. */
659
607
  export function formatToolInputArgs(args: unknown, maxChars = 8_000): string {
660
608
  if (args === undefined || args === null) return "";
@@ -746,6 +694,8 @@ export function renderExpandedToolResult(
746
694
  lastComponent?: unknown,
747
695
  args?: unknown,
748
696
  context?: any,
697
+ /** mode=on 展开卡贴左;compact 等保持默认前导空格 */
698
+ flushLeft = false,
749
699
  ): ExpandedToolIoView | ExpandedToolResultText | Text {
750
700
  const inputBody = formatToolInputArgs(args);
751
701
  const outputBody = body;
@@ -756,7 +706,14 @@ export function renderExpandedToolResult(
756
706
  if (inputBody.trim() || outputBody.trim()) {
757
707
  let view: ExpandedToolIoView;
758
708
  if (isExpandedToolIoView(lastComponent)) {
759
- lastComponent.setContent(inputBody, outputBody, isError, maxOutputLines, maxInputLines);
709
+ lastComponent.setContent(
710
+ inputBody,
711
+ outputBody,
712
+ isError,
713
+ maxOutputLines,
714
+ maxInputLines,
715
+ flushLeft,
716
+ );
760
717
  view = lastComponent;
761
718
  } else {
762
719
  view = new ExpandedToolIoView(
@@ -766,6 +723,7 @@ export function renderExpandedToolResult(
766
723
  isError,
767
724
  maxOutputLines,
768
725
  maxInputLines,
726
+ flushLeft,
769
727
  );
770
728
  }
771
729
  if (context) rememberIoView(context, view);
@@ -803,7 +761,7 @@ function frameViewId(view: ExpandedToolIoView): number | null {
803
761
  function withIoViewMarkers(view: ExpandedToolIoView, lines: string[]): string[] {
804
762
  const id = frameViewId(view);
805
763
  if (id === null) return lines;
806
- // Mark by exact recorded show-more row from render — never scan body text heuristically.
764
+ // Mark by exact header row from render — never scan body text for Input/Output labels.
807
765
  const marked = lines.slice();
808
766
  for (const { section, line } of view.showMoreHeaderLineIndexes()) {
809
767
  if (line < 0 || line >= marked.length) continue;
@@ -36,3 +36,13 @@ export function expandShortcutText(): string {
36
36
  export function showMoreHintText(): string {
37
37
  return isToolTuiFullscreen() ? "click to show more" : `${expandShortcutText()} to show more`;
38
38
  }
39
+
40
+ /** 展开卡的收起提示,与折叠态 showMoreHintText 同一位置对称。 */
41
+ export function collapseHintText(): string {
42
+ return isToolTuiFullscreen() ? "↑ Collapse" : `${expandShortcutText()} to collapse`;
43
+ }
44
+
45
+ /** 该行是否带展开态收起提示(用于展开卡的 hint hover 命中)。 */
46
+ export function isCollapseHintLine(plainLine: string): boolean {
47
+ return plainLine.includes(collapseHintText());
48
+ }
@@ -1,5 +1,7 @@
1
1
  import type { AssistantMessage } from "@earendil-works/pi-ai";
2
2
  import { AssistantMessageComponent, ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
3
+ import { walkComponentTree } from "../utils/component-tree.ts";
4
+ import { COMPACT_MODE_PATCH_KEY, patchRegistry } from "../utils/patch-keys.ts";
3
5
 
4
6
  // pi-coding-agent 类型声明中 AssistantMessageComponent 仅能以 value 形式使用,
5
7
  // updateContent/lastMessage 用结构化类型访问。
@@ -37,8 +39,6 @@ export function refreshTranscriptComponent(value: unknown): boolean {
37
39
  * 遍历 value.children 与 value.getMountedRoots?.()(seen 集合防环),
38
40
  * 单个组件失败用 try/catch 隔离,不影响其余组件。
39
41
  */
40
- // compact-mode 把补丁暴露在此 Symbol 上(见 compact-mode.ts),避免循环依赖。
41
- const COMPACT_MODE_PATCH_KEY = Symbol.for("pi.ccstyle.compact-mode-patch");
42
42
 
43
43
  /**
44
44
  * 补丁链序不变量:compact 模式下 compact-mode 必须位于 compact-thinking
@@ -48,8 +48,10 @@ const COMPACT_MODE_PATCH_KEY = Symbol.for("pi.ccstyle.compact-mode-patch");
48
48
  */
49
49
  function assertCompactModeOutermost(): void {
50
50
  try {
51
- const patch = (globalThis as any)[COMPACT_MODE_PATCH_KEY];
52
- if (typeof patch?.assertAssistantOwnership === "function") {
51
+ const patch = patchRegistry.get<{ assertAssistantOwnership?: () => void }>(
52
+ COMPACT_MODE_PATCH_KEY,
53
+ );
54
+ if (patch && typeof patch.assertAssistantOwnership === "function") {
53
55
  patch.assertAssistantOwnership();
54
56
  }
55
57
  } catch {
@@ -61,31 +63,11 @@ export function refreshMountedTranscript(tui?: unknown): void {
61
63
  if (!tui || typeof (tui as any).getMountedRoots !== "function") return;
62
64
  // 先修正链序再重绘:扫描触发 updateContent 时 compact-mode 已在外层。
63
65
  assertCompactModeOutermost();
64
- const seen = new Set<object>();
65
- const visit = (value: any): void => {
66
- if (!value || typeof value !== "object" || seen.has(value)) return;
67
- seen.add(value);
68
- if (Array.isArray(value)) {
69
- for (const child of value) visit(child);
70
- return;
71
- }
66
+ walkComponentTree(tui, (value: any) => {
72
67
  try {
73
68
  refreshTranscriptComponent(value);
74
69
  } catch {
75
70
  // 单个组件失败不阻断其余组件;后续 session_tree/重绘再试。
76
71
  }
77
- const children = value.children;
78
- if (Array.isArray(children)) {
79
- for (const child of children) visit(child);
80
- }
81
- try {
82
- const mounted = value.getMountedRoots?.();
83
- if (Array.isArray(mounted)) {
84
- for (const root of mounted) visit(root);
85
- }
86
- } catch {
87
- // 惰性 Proxy / 未挂载容器没有 mounted roots。
88
- }
89
- };
90
- visit(tui);
72
+ });
91
73
  }
@@ -0,0 +1,44 @@
1
+ /** ANSI 16 色 → RGB(标准 xterm 色序)。 */
2
+ const ANSI_16_RGB: readonly [number, number, number][] = [
3
+ [0, 0, 0],
4
+ [128, 0, 0],
5
+ [0, 128, 0],
6
+ [128, 128, 0],
7
+ [0, 0, 128],
8
+ [128, 0, 128],
9
+ [0, 128, 128],
10
+ [192, 192, 192],
11
+ [128, 128, 128],
12
+ [255, 0, 0],
13
+ [0, 255, 0],
14
+ [255, 255, 0],
15
+ [0, 0, 255],
16
+ [255, 0, 255],
17
+ [0, 255, 255],
18
+ [255, 255, 255],
19
+ ];
20
+
21
+ /** ANSI 16 色码(0-15)→ RGB;越界回退白。 */
22
+ export function ansi16ToRgb(index: number): [number, number, number] {
23
+ return ANSI_16_RGB[index] ?? [255, 255, 255];
24
+ }
25
+
26
+ /** ANSI 256 色码 → RGB(0-15 标准色 + 232-255 灰阶 + 16-231 六阶立方体)。 */
27
+ export function ansi256ToRgb(code: number): [number, number, number] {
28
+ // SGR 正则可解析任意位数字,入口先规范:取整并钳制到合法区间,
29
+ // 否则灰阶分支对 code > 255 会算出超出 0-255 的非法 RGB。
30
+ if (!Number.isFinite(code)) return [0, 0, 0];
31
+ code = Math.min(255, Math.max(0, Math.trunc(code)));
32
+ if (code < 16) return ansi16ToRgb(code);
33
+ if (code >= 232) {
34
+ const gray = 8 + (code - 232) * 10;
35
+ return [gray, gray, gray];
36
+ }
37
+ const cubeIndex = code - 16;
38
+ const levels = [0, 95, 135, 175, 215, 255];
39
+ return [
40
+ levels[Math.floor(cubeIndex / 36)]!,
41
+ levels[Math.floor((cubeIndex % 36) / 6)]!,
42
+ levels[cubeIndex % 6]!,
43
+ ];
44
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * 顶层共享的 ANSI/终端控制序列处理。
3
+ *
4
+ * 收敛原先散落在 renderer/grouping.ts / renderer/mouse/packets.ts /
5
+ * renderer/compact-mode.ts 的重复剥离逻辑,供 renderer 与 feature 共用。
6
+ */
7
+
8
+ /** 单个 CSI 序列(颜色、光标等 SGR/CUP/ED 等)。 */
9
+ const CSI_SEQUENCE_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
10
+ /** OSC 序列(如 \x1b]8;;url\x07 或 ST 结尾)。 */
11
+ const OSC_SEQUENCE_RE = /\x1b\][^\x07]*(?:\x07|\x1b\\)/g;
12
+ /** compact-mode 原有 hasVisibleText 用的更严格 OSC(内容不允许内嵌 ESC)。 */
13
+ const OSC_SEQUENCE_STRICT_RE = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
14
+
15
+ /** 剥离一行内所有 CSI 序列。 */
16
+ export function stripAnsi(line: string): string {
17
+ return line.replace(CSI_SEQUENCE_RE, "");
18
+ }
19
+
20
+ /** 仅剥离终端序列、保留原布局(换行/空白不动),用于命中区间计算。 */
21
+ export function stripTerminalSequencesPreservingLayout(value: string): string {
22
+ return value.replace(OSC_SEQUENCE_RE, "").replace(CSI_SEQUENCE_RE, "");
23
+ }
24
+
25
+ /** 剥离终端序列并折叠空白(用于纯文本比较)。 */
26
+ export function stripTerminalSequences(value: string): string {
27
+ return stripTerminalSequencesPreservingLayout(value).replace(/\s+/g, " ").trim();
28
+ }
29
+
30
+ /** 去掉行内所有 CSI/OSC 序列后是否仍有可见文本(判断工具卡首尾内容行)。 */
31
+ export function hasVisibleText(line: string): boolean {
32
+ // OSC 用更严格的变体:内容内嵌 ESC 时不吞并后续文本,保持 compact-mode 原有语义。
33
+ return line.replace(CSI_SEQUENCE_RE, "").replace(OSC_SEQUENCE_STRICT_RE, "").trim().length > 0;
34
+ }
35
+
36
+ /** 剥离背景色 ANSI(用于重新铺背景行)。 */
37
+ export function stripBackgroundAnsi(line: string): string {
38
+ return line.replace(/\x1b\[(?:4[0-9]|10[0-7]|48(?:(?:;|:)[0-9]+)+|49)m/g, "");
39
+ }
40
+
41
+ /** 剥离行首状态图标(展开组内工具首行复用)。 */
42
+ export function stripLeadingStatusIcon(line: string): string {
43
+ return line.replace(
44
+ /^((?:\x1b\[[0-9;]*m|[ \t]|[├└│─])*)(?:\x1b\[[0-9;]*m)*(?:[✓✗●○■⬤•·])(?:\x1b\[[0-9;]*m)*\s+/,
45
+ "$1",
46
+ );
47
+ }
48
+
49
+ /** 终端序列零宽(不计入 plain 下标),与 stripAnsi/stripTerminalSequencesPreservingLayout 一致。 */
50
+ const ANY_SEQUENCE_RE = new RegExp(`${CSI_SEQUENCE_RE.source}|${OSC_SEQUENCE_RE.source}`, "g");
51
+
52
+ /** 只保留 [start, end) 之外的字符(base 为 text 首字符在 plain 串中的下标)。 */
53
+ function keepOutsideRange(text: string, base: number, start: number, end: number): string {
54
+ if (base + text.length <= start || base >= end) return text;
55
+ return text.slice(0, Math.max(0, start - base)) + text.slice(Math.max(0, end - base));
56
+ }
57
+
58
+ /**
59
+ * 删除 plain 区间 [start, end) 的字符,保留其余字符的 ANSI 样式。
60
+ * 区间内的终端序列原样保留,因此区间后的字符(如收尾括号)沿用前文颜色,
61
+ * 不会像截断后拼纯文本那样掉回终端默认前景色。
62
+ */
63
+ export function removeStyledRange(styled: string, start: number, end: number): string {
64
+ if (end <= start) return styled;
65
+ let plain = 0;
66
+ let cursor = 0;
67
+ let out = "";
68
+ for (const match of styled.matchAll(ANY_SEQUENCE_RE)) {
69
+ const text = styled.slice(cursor, match.index);
70
+ out += keepOutsideRange(text, plain, start, end);
71
+ plain += text.length;
72
+ out += match[0];
73
+ cursor = match.index + match[0].length;
74
+ }
75
+ return out + keepOutsideRange(styled.slice(cursor), plain, start, end);
76
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * 组件树递归遍历共享实现。
3
+ *
4
+ * 收敛原先散落在 renderer 的 compact-mode / message-display / transcript-refresh /
5
+ * grouping / mouse/packets 的「children + getMountedRoots + seen 防环」同款遍历。
6
+ *
7
+ * visitor 在每个非数组对象节点上调用一次;返回 `false` 表示不再下钻该节点的
8
+ * children 与 mounted roots(对应各处原有的提前 return 语义)。
9
+ */
10
+ export function walkComponentTree(root: any, visitor: (value: any) => boolean | void): void {
11
+ const seen = new Set<any>();
12
+ const visit = (value: any): void => {
13
+ if (!value || typeof value !== "object" || seen.has(value)) return;
14
+ seen.add(value);
15
+ if (Array.isArray(value)) {
16
+ for (const child of value) visit(child);
17
+ return;
18
+ }
19
+ if (visitor(value) === false) return;
20
+ const children = value.children;
21
+ if (Array.isArray(children)) {
22
+ for (const child of children) visit(child);
23
+ }
24
+ try {
25
+ const mounted = value.getMountedRoots?.();
26
+ if (Array.isArray(mounted)) {
27
+ for (const root of mounted) visit(root);
28
+ }
29
+ } catch {
30
+ // renderer 切换中的惰性 Proxy 可能暂时没有 mounted roots。
31
+ }
32
+ };
33
+ visit(root);
34
+ }