pi-one-ui 0.1.1

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 (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/extensions/app/config/renderer.ts +452 -0
  4. package/extensions/app/config/shell.ts +2484 -0
  5. package/extensions/app/ownership.ts +18 -0
  6. package/extensions/app/panel.ts +533 -0
  7. package/extensions/app/presets.ts +36 -0
  8. package/extensions/features/agent-summary/core.ts +194 -0
  9. package/extensions/features/agent-summary/index.ts +37 -0
  10. package/extensions/features/compact-thinking.ts +1262 -0
  11. package/extensions/features/context.ts +857 -0
  12. package/extensions/features/reference/index.ts +643 -0
  13. package/extensions/features/reference/session.ts +278 -0
  14. package/extensions/features/reference/subagent.ts +218 -0
  15. package/extensions/features/shell/aliases.ts +28 -0
  16. package/extensions/features/shell/flush-docked-bash.ts +39 -0
  17. package/extensions/features/shell/startup-header.ts +357 -0
  18. package/extensions/features/shell/working-message.ts +226 -0
  19. package/extensions/index.ts +52 -0
  20. package/extensions/shell/accent-rail-editor.ts +112 -0
  21. package/extensions/shell/accent-rail-layout-patch.ts +530 -0
  22. package/extensions/shell/completion-menu.ts +186 -0
  23. package/extensions/shell/editor-metadata-format.ts +287 -0
  24. package/extensions/shell/editor-transfer.ts +73 -0
  25. package/extensions/shell/extension-status.ts +102 -0
  26. package/extensions/shell/footer-format.ts +388 -0
  27. package/extensions/shell/footer-layout.ts +144 -0
  28. package/extensions/shell/footer.ts +1035 -0
  29. package/extensions/shell/format.ts +577 -0
  30. package/extensions/shell/git.ts +434 -0
  31. package/extensions/shell/icons.ts +261 -0
  32. package/extensions/shell/index.ts +1659 -0
  33. package/extensions/shell/interaction-summary.ts +1194 -0
  34. package/extensions/shell/live-context.ts +82 -0
  35. package/extensions/shell/minimalist-editor.ts +548 -0
  36. package/extensions/shell/package-version.ts +675 -0
  37. package/extensions/shell/project-refresh.ts +130 -0
  38. package/extensions/shell/project-state.ts +60 -0
  39. package/extensions/shell/prototype-patch-registry.ts +183 -0
  40. package/extensions/shell/runtime.ts +915 -0
  41. package/extensions/shell/selector-border.ts +109 -0
  42. package/extensions/shell/session-lifecycle.ts +60 -0
  43. package/extensions/shell/settings-command.ts +2144 -0
  44. package/extensions/shell/settings-previews.ts +137 -0
  45. package/extensions/shell/state.ts +83 -0
  46. package/extensions/shell/style.ts +480 -0
  47. package/extensions/shell/telemetry.ts +90 -0
  48. package/extensions/shell/ui.ts +1326 -0
  49. package/extensions/shell/user-message-osc.ts +279 -0
  50. package/extensions/shell/user-message-styles.ts +266 -0
  51. package/extensions/shell/user-message.ts +226 -0
  52. package/extensions/shell/working-line-messages.ts +24 -0
  53. package/extensions/shell/working-line-spinners.ts +54 -0
  54. package/extensions/shell/working-line.ts +1603 -0
  55. package/extensions/tools/ansi-color.ts +44 -0
  56. package/extensions/tools/ansi-text.ts +52 -0
  57. package/extensions/tools/component-tree.ts +37 -0
  58. package/extensions/tools/format.ts +28 -0
  59. package/extensions/tools/fullscreen-detect.ts +11 -0
  60. package/extensions/tools/patch-keys.ts +158 -0
  61. package/extensions/tools/sgr-mouse.ts +59 -0
  62. package/extensions/tools/tool-loading-icon.ts +20 -0
  63. package/extensions/tools/tool-result-sanitize.ts +27 -0
  64. package/extensions/transcript/index.ts +65 -0
  65. package/extensions/transcript/renderer/compact-mode.ts +1443 -0
  66. package/extensions/transcript/renderer/default-mode.ts +735 -0
  67. package/extensions/transcript/renderer/index.ts +286 -0
  68. package/extensions/transcript/renderer/markdown-enhance.ts +325 -0
  69. package/extensions/transcript/renderer/mouse/hover.ts +175 -0
  70. package/extensions/transcript/renderer/mouse/interaction.ts +1016 -0
  71. package/extensions/transcript/renderer/mouse/layout.ts +207 -0
  72. package/extensions/transcript/renderer/mouse/packets.ts +96 -0
  73. package/extensions/transcript/renderer/mouse/scroll.ts +211 -0
  74. package/extensions/transcript/renderer/tool/diff/ATTRIBUTION.md +28 -0
  75. package/extensions/transcript/renderer/tool/diff/ansi-utils.ts +128 -0
  76. package/extensions/transcript/renderer/tool/diff/diff-component.ts +112 -0
  77. package/extensions/transcript/renderer/tool/diff/diff-edit-render.ts +255 -0
  78. package/extensions/transcript/renderer/tool/diff/diff-header.ts +184 -0
  79. package/extensions/transcript/renderer/tool/diff/diff-highlight.ts +131 -0
  80. package/extensions/transcript/renderer/tool/diff/diff-inline.ts +350 -0
  81. package/extensions/transcript/renderer/tool/diff/diff-layout.ts +870 -0
  82. package/extensions/transcript/renderer/tool/diff/diff-limits.ts +189 -0
  83. package/extensions/transcript/renderer/tool/diff/diff-palette.ts +436 -0
  84. package/extensions/transcript/renderer/tool/diff/diff-parse.ts +536 -0
  85. package/extensions/transcript/renderer/tool/diff/diff-presentation.ts +79 -0
  86. package/extensions/transcript/renderer/tool/diff/diff-renderer.ts +29 -0
  87. package/extensions/transcript/renderer/tool/diff/diff-text.ts +57 -0
  88. package/extensions/transcript/renderer/tool/diff/diff-write-render.ts +629 -0
  89. package/extensions/transcript/renderer/tool/diff/index.ts +147 -0
  90. package/extensions/transcript/renderer/tool/diff/line-width-safety.ts +100 -0
  91. package/extensions/transcript/renderer/tool/diff/shiki-highlight.ts +104 -0
  92. package/extensions/transcript/renderer/tool/diff/write-execution.ts +124 -0
  93. package/extensions/transcript/renderer/tool/grouping.ts +619 -0
  94. package/extensions/transcript/renderer/tool/message-display.ts +183 -0
  95. package/extensions/transcript/renderer/tool/names.ts +195 -0
  96. package/extensions/transcript/renderer/tool/result.ts +842 -0
  97. package/extensions/transcript/renderer/tool/show-more-hint.ts +42 -0
  98. package/extensions/transcript/renderer/transcript-refresh.ts +79 -0
  99. package/package.json +58 -0
  100. package/themes/cc-dark.json +91 -0
  101. package/themes/cc-light.json +91 -0
@@ -0,0 +1,1016 @@
1
+ import {
2
+ hasActiveTextPreview,
3
+ showTextPreview,
4
+ } from "../../../features/context.ts";
5
+ import { ThinkingPreviewBlock } from "../../../features/compact-thinking.ts";
6
+ import {
7
+ patchRegistry,
8
+ TOOL_MOUSE_OWNER_KEY,
9
+ } from "../../../tools/patch-keys.ts";
10
+ import { ToolGroupComponent } from "../tool/grouping.ts";
11
+ import {
12
+ isCompactAssistantComponent,
13
+ setHoveredCompactAssistant,
14
+ } from "../compact-mode.ts";
15
+ import { isMessageDisplayComponent } from "../tool/message-display.ts";
16
+ import { config } from "../../../app/config/renderer.ts";
17
+ import { isLazyProxyTui } from "../../../tools/fullscreen-detect.ts";
18
+ import { setToolTuiFullscreen } from "../tool/show-more-hint.ts";
19
+ import {
20
+ type ExpandedToolIoView,
21
+ getActiveIoViewFrame,
22
+ isExpandedToolIoView,
23
+ type IoViewFrameState,
24
+ setActiveIoViewFrame,
25
+ type ToolIoSection,
26
+ } from "../tool/result.ts";
27
+ import {
28
+ collectToolComponents,
29
+ extractToolFramePlacements,
30
+ isSgrLeftPress,
31
+ isToolExecutionComponent,
32
+ parseSgrMousePackets,
33
+ stripTerminalSequences,
34
+ stripTerminalSequencesPreservingLayout,
35
+ toolFrameMarker,
36
+ type FrameToolPlacement,
37
+ type SgrMousePacket,
38
+ } from "./packets.ts";
39
+ import {
40
+ collectFullscreenToolCards,
41
+ componentAtLocalRow,
42
+ collapsedHintHitbox,
43
+ fullscreenContentWidth,
44
+ fullscreenLeafAt,
45
+ isScrollbarColumnAt,
46
+ } from "./layout.ts";
47
+ import {
48
+ fullscreenLazyTui,
49
+ hideScrollButton,
50
+ isScrollBottomInput,
51
+ renderScrollButton,
52
+ resetScrollButtonState,
53
+ scheduleScrollButtonSync,
54
+ setScrollButtonHovered,
55
+ setScrollButtonVisible,
56
+ setScrollButtonWidget,
57
+ setToolMouseTui,
58
+ getScrollButtonVisible,
59
+ getScrollButtonWidget,
60
+ getToolMouseTui,
61
+ toolMouseInteractionActive,
62
+ updateScrollButtonFromInput,
63
+ } from "./scroll.ts";
64
+ import {
65
+ applyFullscreenHover,
66
+ cachedFullscreenComponentAtRow,
67
+ sharedToolHoverState,
68
+ setHoveredToolCallId,
69
+ setHoveredToolGroup,
70
+ setHoveredThinking,
71
+ setHoveredMessageDisplay,
72
+ setHoveredToolIo,
73
+ type FullscreenHoverTarget,
74
+ } from "./hover.ts";
75
+
76
+ type FrameToolRender = {
77
+ component: any;
78
+ lines: string[];
79
+ contentBoxLines: number;
80
+ };
81
+
82
+ type InteractionRegion = {
83
+ kind: "collapsed-hint" | "expanded-card" | "show-more" | "scroll-bottom";
84
+ row: number;
85
+ startCol: number;
86
+ endCol: number;
87
+ component?: any;
88
+ view?: ExpandedToolIoView;
89
+ section?: ToolIoSection;
90
+ };
91
+
92
+ type InteractionFrame = { regions: InteractionRegion[] };
93
+
94
+ const TOOL_MOUSE_WIDGET_KEY = "ccstyle-tool-mouse";
95
+ const TOOL_MOUSE_MOTION_ENABLE = "\x1b[?1003h\x1b[?1006h";
96
+ const TOOL_MOUSE_MOTION_DISABLE = "\x1b[?1003l";
97
+ const FULLSCREEN_MOTION_ENABLED = Symbol("ccstyle.fullscreen-motion-enabled");
98
+ const DEFAULT_TOOL_MOUSE_OWNER = {};
99
+ export const TOOL_MOUSE_DISABLE = "\x1b[?1006l\x1b[?1003l\x1b[?1000l";
100
+
101
+ let toolMouseUi: any = null;
102
+ let toolMouseInputUnsubscribe: (() => void) | null = null;
103
+ let toolMouseRenderPatchTui: any = null;
104
+ let toolMouseRenderPatchOriginal: ((...args: any[]) => any) | null = null;
105
+ let toolMouseRenderPatchWrapper: ((...args: any[]) => any) | null = null;
106
+ let toolMouseRenderPatchState: { active: boolean } | null = null;
107
+ let toolMouseRawWrite: ((data: string) => unknown) | null = null;
108
+ let toolMouseInstallationOwner: object | null = null;
109
+ let fullscreenMotionTerminal: any = null;
110
+ let ownsFullscreenMotion = false;
111
+ let sessionRenderTimer: ReturnType<typeof setTimeout> | null = null;
112
+ let latestInteractionFrame: InteractionFrame = { regions: [] };
113
+
114
+ /** Summary markers used by Pi and ccstyle; unlike the trailing hint, these survive truncation. */
115
+ const COLLAPSED_TOOL_SUMMARY = /^\s*(?:↳|└|⎿|●|✓|✗|…)/;
116
+
117
+ function interactionRegionAt(packet: SgrMousePacket): InteractionRegion | null {
118
+ const matches = latestInteractionFrame.regions.filter(
119
+ (region) =>
120
+ region.row === packet.row &&
121
+ packet.col >= region.startCol &&
122
+ packet.col <= region.endCol,
123
+ );
124
+ return (
125
+ matches.find((region) => region.kind === "show-more") ??
126
+ matches.find((region) => region.kind === "scroll-bottom") ??
127
+ matches.find((region) => region.kind === "collapsed-hint") ??
128
+ matches.find((region) => region.kind === "expanded-card") ??
129
+ null
130
+ );
131
+ }
132
+
133
+ function tryOpenToolIoShowMore(region: InteractionRegion): boolean {
134
+ const ioView = region.view;
135
+ const section = region.section;
136
+ if (!ioView || !section) return false;
137
+ const ui = toolMouseUi;
138
+ if (!ui || typeof ui.custom !== "function") {
139
+ ui?.notify?.("Full preview requires TUI custom UI", "warning");
140
+ return true;
141
+ }
142
+ const title = section === "input" ? "Tool Input" : "Tool Output";
143
+ const content =
144
+ section === "input" ? ioView.getInputBody() : ioView.getOutputBody();
145
+ void showTextPreview({ ui }, title, content || "(empty)");
146
+ return true;
147
+ }
148
+
149
+ function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
150
+ if ((packet.code & 32) === 0 || packet.final !== "M") return;
151
+ const region = interactionRegionAt(packet);
152
+ const nextScrollButtonHovered = region?.kind === "scroll-bottom";
153
+ const scrollButtonChanged = setScrollButtonHovered(nextScrollButtonHovered);
154
+ const component = region?.component;
155
+ const nextToolCallId =
156
+ region?.kind === "collapsed-hint" ? (component?.toolCallId ?? null) : null;
157
+ const nextGroup = component instanceof ToolGroupComponent ? component : null;
158
+ const nextIoView =
159
+ region?.kind === "show-more" ? (region.view ?? null) : null;
160
+ const nextIoSection =
161
+ region?.kind === "show-more" ? (region.section ?? null) : null;
162
+ const changed = nextToolCallId !== sharedToolHoverState().toolCallId;
163
+ setHoveredToolCallId(nextToolCallId);
164
+ if (
165
+ scrollButtonChanged ||
166
+ setHoveredToolIo(nextIoView, nextIoSection) ||
167
+ setHoveredToolGroup(nextGroup) ||
168
+ changed
169
+ )
170
+ tui.requestRender?.();
171
+ }
172
+
173
+ const EXPAND_PANEL_DOUBLE_CLICK_MS = 400;
174
+ let lastExpandPanelClick: { id: unknown; at: number } | null = null;
175
+
176
+ function expandPanelIdentity(card: any): unknown {
177
+ return card instanceof ThinkingPreviewBlock
178
+ ? `${card.messageTimestamp}:${card.runStartIndex}`
179
+ : card;
180
+ }
181
+
182
+ function isExpandPanelDoubleClick(card: any): boolean {
183
+ const now = Date.now();
184
+ const id = expandPanelIdentity(card);
185
+ const prev = lastExpandPanelClick;
186
+ lastExpandPanelClick = { id, at: now };
187
+ return Boolean(
188
+ prev && prev.id === id && now - prev.at <= EXPAND_PANEL_DOUBLE_CLICK_MS,
189
+ );
190
+ }
191
+
192
+ function clearExpandPanelDoubleClick(): void {
193
+ lastExpandPanelClick = null;
194
+ }
195
+
196
+ function collapseExpandedCard(tui: any, card: any): boolean {
197
+ if (!isExpandPanelDoubleClick(card)) return false;
198
+ card.setExpanded(false);
199
+ setHoveredToolCallId(null);
200
+ setHoveredToolGroup(null);
201
+ setHoveredThinking(null);
202
+ setHoveredMessageDisplay(null);
203
+ setHoveredToolIo(null, null);
204
+ setHoveredCompactAssistant(null);
205
+ card.invalidate?.();
206
+ tui.requestRender?.();
207
+ clearExpandPanelDoubleClick();
208
+ return true;
209
+ }
210
+
211
+ function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
212
+ const region = interactionRegionAt(packet);
213
+ if (!region) return false;
214
+ if (region.kind === "scroll-bottom") return false;
215
+ if (region.kind === "show-more") return tryOpenToolIoShowMore(region);
216
+ const component = region.component;
217
+ if (!component) return false;
218
+ if (region.kind === "expanded-card")
219
+ return collapseExpandedCard(tui, component);
220
+ component.setExpanded(true);
221
+ clearExpandPanelDoubleClick();
222
+ setHoveredToolCallId(null);
223
+ setHoveredToolGroup(null);
224
+ setHoveredToolIo(null, null);
225
+ component.invalidate?.();
226
+ tui.requestRender?.();
227
+ return true;
228
+ }
229
+
230
+ function officialFullscreenHasAllMotion(): boolean {
231
+ const term = process.env.TERM?.toLowerCase() ?? "";
232
+ return !(
233
+ process.env.TMUX !== undefined ||
234
+ process.env.ZELLIJ !== undefined ||
235
+ process.env.STY !== undefined ||
236
+ term.startsWith("tmux") ||
237
+ term.startsWith("screen")
238
+ );
239
+ }
240
+
241
+ /**
242
+ * hover 依赖 DECSET 1003。官方 fullscreen 在 multiplexer 下只开 1002,
243
+ * 因此扩展需在每个实际 renderer 上补开(Symbol 经惰性 Proxy 落到当前实例)。
244
+ */
245
+ function ensureFullscreenToolMouseMotion(tui: any): void {
246
+ setToolTuiFullscreen(fullscreenLazyTui(tui));
247
+ if (!fullscreenLazyTui(tui)) {
248
+ releaseFullscreenToolMouseMotion(tui);
249
+ return;
250
+ }
251
+ // 面板改 scrollStepLines 后,下一帧渲染即同步(restore 仍按 original 恢复)。
252
+ if (
253
+ typeof tui.wheelScrollLines === "number" &&
254
+ tui.wheelScrollLines !== config.scrollStepLines
255
+ ) {
256
+ tui.wheelScrollLines = config.scrollStepLines;
257
+ }
258
+ if (
259
+ !toolMouseInteractionActive() ||
260
+ tui.mouseEnabled === false ||
261
+ tui.altScreenActive === false ||
262
+ tui[FULLSCREEN_MOTION_ENABLED]
263
+ ) {
264
+ return;
265
+ }
266
+ try {
267
+ tui.terminal?.write?.(TOOL_MOUSE_MOTION_ENABLE);
268
+ tui[FULLSCREEN_MOTION_ENABLED] = true;
269
+ fullscreenMotionTerminal = tui.terminal;
270
+ ownsFullscreenMotion = !officialFullscreenHasAllMotion();
271
+ } catch {
272
+ // renderer 可能正在切换或终端已经关闭。
273
+ }
274
+ }
275
+
276
+ function releaseFullscreenToolMouseMotion(tui?: any): void {
277
+ try {
278
+ if (tui?.[FULLSCREEN_MOTION_ENABLED])
279
+ tui[FULLSCREEN_MOTION_ENABLED] = false;
280
+ } catch {
281
+ // 惰性 Proxy 可能已经切到另一个 renderer。
282
+ }
283
+ const terminal = fullscreenMotionTerminal;
284
+ const shouldDisable = ownsFullscreenMotion;
285
+ fullscreenMotionTerminal = null;
286
+ ownsFullscreenMotion = false;
287
+ try {
288
+ if (shouldDisable) terminal?.write?.(TOOL_MOUSE_MOTION_DISABLE);
289
+ } catch {
290
+ // renderer 可能正在切换或终端已经关闭。
291
+ }
292
+ }
293
+
294
+ const FULLSCREEN_VIEWPORT_PATCH = Symbol("ccstyle.fullscreen-viewport-patch");
295
+ const FULLSCREEN_WHEEL_SCROLL_ORIGINAL = Symbol(
296
+ "ccstyle.fullscreen-wheel-scroll-original",
297
+ );
298
+
299
+ /**
300
+ * 官方 fullscreen 工具卡点击:collapsed hint 单击展开
301
+ * (有且仅保持一个展开:展开前收起其他工具卡),expanded 整卡双击收起,
302
+ * 截断头 show-more 单击打开全量预览;回到底部按钮 scrollToBottom。
303
+ * 滚动条列、含 OSC8 链接行、非工具区域、展开卡单击放行官方。
304
+ */
305
+ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
306
+ const layout = tui.currentLayout;
307
+ if (!layout?.root) return false;
308
+ // 官方事件坐标 0-based;SGR packet 1-based。
309
+ const x = packet.col - 1;
310
+ const y = packet.row - 1;
311
+ if (isScrollbarColumnAt(layout, x)) return false;
312
+ const hit = fullscreenLeafAt(layout, x, y);
313
+ if (!hit) return false;
314
+ const width = Math.max(1, Number(tui.terminal?.columns) || 80);
315
+ // 布局树用 scroll 的 contentWidth 渲染内容(滚动条占用时 = width-1);
316
+ // 行号定位必须用同一宽度,否则换行差异导致组件行错位。
317
+ const contentWidth = fullscreenContentWidth(hit.box, width);
318
+ const target = componentAtLocalRow(
319
+ hit.box.component,
320
+ hit.localRow,
321
+ contentWidth,
322
+ );
323
+ if (!target) return false;
324
+ const component = target.component;
325
+ const card = target.group ?? component;
326
+ // 回到底部按钮:按组件引用命中,不依赖渲染行缓存。
327
+ if (getScrollButtonVisible() && component === getScrollButtonWidget()) {
328
+ tui.scrollToBottom?.();
329
+ hideScrollButton(tui);
330
+ return true;
331
+ }
332
+ const line = hit.box.lines?.[hit.localRow];
333
+ if (typeof line !== "string" || /\x1b]8;[^;]*;/.test(line)) return false;
334
+ const isTool = isToolExecutionComponent(component);
335
+ const isGroup = component instanceof ToolGroupComponent;
336
+ const isAssistant = isCompactAssistantComponent(component);
337
+ const isThinking = component instanceof ThinkingPreviewBlock;
338
+ const isMessage = isMessageDisplayComponent(component);
339
+ if (!isTool && !isGroup && !isAssistant && !isThinking && !isMessage)
340
+ return false;
341
+ if (!component.expanded) {
342
+ // collapsed 仅按钮文本可展开,不能把同一行正文/留白变成点击区。
343
+ const hint = collapsedHintHitbox(line);
344
+ if (!hint || packet.col < hint.startCol || packet.col > hint.endCol)
345
+ return false;
346
+ // single-expand:展开前收起其他已展开工具卡/group。
347
+ const others: any[] = [];
348
+ collectFullscreenToolCards(hit.box.component, others);
349
+ for (const other of others) {
350
+ if (other !== component && other.expanded) {
351
+ // 展开 round 内 thinking 时不要把外层 compact 卡收起。
352
+ if (isThinking && isCompactAssistantComponent(other)) continue;
353
+ other.setExpanded(false);
354
+ other.invalidate?.();
355
+ }
356
+ }
357
+ component.setExpanded(true);
358
+ clearExpandPanelDoubleClick();
359
+ } else {
360
+ // 普通工具截断头 show-more:打开全量预览(不收起)。
361
+ const view = isTool ? component.resultRendererComponent : null;
362
+ if (isExpandedToolIoView(view)) {
363
+ const plain = stripTerminalSequencesPreservingLayout(line);
364
+ const section = view.matchShowMoreLine(plain);
365
+ if (section) {
366
+ const box = view.showMoreHitbox(plain);
367
+ if (box && x + 1 >= box.startCol && x + 1 <= box.endCol) {
368
+ return tryOpenToolIoShowMore({
369
+ kind: "show-more",
370
+ row: 0,
371
+ startCol: box.startCol,
372
+ endCol: box.endCol,
373
+ component,
374
+ view,
375
+ section,
376
+ });
377
+ }
378
+ }
379
+ }
380
+ // 双击收起:内部工具仍归所属 group。单击放行官方(选择/链接)。
381
+ return collapseExpandedCard(tui, card);
382
+ }
383
+ // 点击后清 hover 高亮。
384
+ setHoveredToolCallId(null);
385
+ setHoveredToolGroup(null);
386
+ setHoveredThinking(null);
387
+ setHoveredMessageDisplay(null);
388
+ setHoveredToolIo(null, null);
389
+ setHoveredCompactAssistant(null);
390
+ card.invalidate?.();
391
+ tui.requestRender?.();
392
+ return true;
393
+ }
394
+
395
+ /**
396
+ * fullscreen 鼠标悬停:collapsed 卡 [click to show more] hint、
397
+ * expanded 卡截断头 show-more、回到底部按钮。motion 不 consume,官方链照常。
398
+ */
399
+ function handleFullscreenToolHover(tui: any, packet: SgrMousePacket): void {
400
+ if (packet.final !== "M") return;
401
+ const layout = tui.currentLayout;
402
+ if (!layout?.root) return;
403
+ const x = packet.col - 1;
404
+ const y = packet.row - 1;
405
+ let target: FullscreenHoverTarget | null = null;
406
+ const hit = fullscreenLeafAt(layout, x, y);
407
+ if (hit) {
408
+ const line = hit.box.lines?.[hit.localRow];
409
+ // 回到底部按钮:渲染行文本 + 列区间识别(零组件树开销)。
410
+ if (
411
+ typeof line === "string" &&
412
+ getScrollButtonVisible() &&
413
+ line.includes("[ ↓")
414
+ ) {
415
+ const plain = stripTerminalSequencesPreservingLayout(line);
416
+ const idx = plain.indexOf("[ ↓");
417
+ if (idx >= 0 && x >= idx && x <= idx + plain.length - 1) {
418
+ target = { kind: "button" };
419
+ }
420
+ } else if (typeof line === "string" && !/\x1b]8;/.test(line)) {
421
+ const width = Math.max(1, Number(tui.terminal?.columns) || 80);
422
+ const contentWidth = fullscreenContentWidth(hit.box, width);
423
+ // 与点击共用同一定位算法,避免 hover 自建行段与真实组件树错位。
424
+ const componentHit = cachedFullscreenComponentAtRow(
425
+ layout,
426
+ hit.box.component,
427
+ hit.localRow,
428
+ contentWidth,
429
+ );
430
+ const component = componentHit?.component;
431
+ const hintBox = collapsedHintHitbox(line);
432
+ const overHint = Boolean(
433
+ hintBox &&
434
+ packet.col >= hintBox.startCol &&
435
+ packet.col <= hintBox.endCol,
436
+ );
437
+ if (component instanceof ToolGroupComponent) {
438
+ if (overHint) target = { kind: "group", component };
439
+ } else if (component instanceof ThinkingPreviewBlock) {
440
+ if (component.expanded || overHint)
441
+ target = { kind: "thinking", component };
442
+ } else if (isToolExecutionComponent(component)) {
443
+ let view: ExpandedToolIoView | null = null;
444
+ let section: ToolIoSection | null = null;
445
+ if (component.expanded) {
446
+ const resultView = component.resultRendererComponent;
447
+ if (isExpandedToolIoView(resultView)) {
448
+ view = resultView;
449
+ const plain = stripTerminalSequencesPreservingLayout(line);
450
+ const candidate = view.matchShowMoreLine(plain);
451
+ if (candidate) {
452
+ const box = view.showMoreHitbox(plain);
453
+ if (box && x + 1 >= box.startCol && x + 1 <= box.endCol) {
454
+ section = candidate;
455
+ }
456
+ }
457
+ }
458
+ target = { kind: "tool", component, view, section };
459
+ } else if (overHint) {
460
+ target = { kind: "tool", component, view, section };
461
+ }
462
+ } else if (isCompactAssistantComponent(component)) {
463
+ // compact 摘要行:折叠时仅提示文字高亮,展开卡整体高亮。
464
+ if (component.expanded || overHint)
465
+ target = { kind: "assistant", component };
466
+ } else if (isMessageDisplayComponent(component)) {
467
+ if (overHint) target = { kind: "message", component };
468
+ }
469
+ }
470
+ }
471
+ applyFullscreenHover(tui, target);
472
+ }
473
+
474
+ /**
475
+ * 实例级包装 TuiAltScreen.handleViewportInput(惰性 Proxy 安全):
476
+ * 原型方法取 original(绕开 proxy 函数包装),实例 own property 装 wrapper
477
+ * (constructor arrow 动态查找命中)。仅在 fullscreen 且无 overlay 时先消费
478
+ * 工具卡左键点击,其余全部放行官方 selection/scrollbar/URL/键盘链。
479
+ */
480
+ function patchFullscreenViewportInput(tui: any): void {
481
+ if (tui[FULLSCREEN_VIEWPORT_PATCH] || !isLazyProxyTui(tui)) return;
482
+ const proto = Object.getPrototypeOf(tui);
483
+ const original = proto?.handleViewportInput;
484
+ if (typeof original !== "function") return;
485
+ // 官方原生 routeWheel 已完整处理嵌套 ScrollView;只调整默认步进(config.scrollStepLines)。
486
+ if (typeof tui.wheelScrollLines === "number") {
487
+ tui[FULLSCREEN_WHEEL_SCROLL_ORIGINAL] = tui.wheelScrollLines;
488
+ tui.wheelScrollLines = config.scrollStepLines;
489
+ }
490
+ tui[FULLSCREEN_VIEWPORT_PATCH] = true;
491
+ tui.handleViewportInput = function (this: any, data: string) {
492
+ if (toolMouseInteractionActive() && tui.mode === "fullscreen") {
493
+ // 滚动输入(wheel/pageUp/end 等)后同步回到底部按钮显隐;
494
+ // 官方 viewport 会消费键盘,扩展监听器无法补偿,必须在这里调度。
495
+ scheduleScrollButtonSync(tui, data);
496
+ const packets = parseSgrMousePackets(data);
497
+ // 官方 fullscreen 会消费全部鼠标;文本预览 overlay 活动时放行给 focused
498
+ // custom component,使 [esc] 点击和滚轮可用。
499
+ if (packets && tui.hasOverlay?.() && hasActiveTextPreview())
500
+ return undefined;
501
+ if (packets && !tui.hasOverlay?.()) {
502
+ for (const packet of packets) {
503
+ if (
504
+ isSgrLeftPress(packet) &&
505
+ handleFullscreenToolClick(tui, packet)
506
+ ) {
507
+ return { consume: true };
508
+ }
509
+ if ((packet.code & 32) !== 0 && packet.final === "M") {
510
+ handleFullscreenToolHover(tui, packet);
511
+ }
512
+ }
513
+ }
514
+ }
515
+ return Reflect.apply(original, this, [data]);
516
+ };
517
+ }
518
+
519
+ function restoreFullscreenViewportInput(tui: any): void {
520
+ if (!tui || !tui[FULLSCREEN_VIEWPORT_PATCH]) return;
521
+ const proto = Object.getPrototypeOf(tui);
522
+ if (typeof proto?.handleViewportInput === "function") {
523
+ tui.handleViewportInput = proto.handleViewportInput;
524
+ }
525
+ const originalWheelScrollLines = tui[FULLSCREEN_WHEEL_SCROLL_ORIGINAL];
526
+ if (typeof originalWheelScrollLines === "number") {
527
+ tui.wheelScrollLines = originalWheelScrollLines;
528
+ tui[FULLSCREEN_WHEEL_SCROLL_ORIGINAL] = undefined;
529
+ }
530
+ tui[FULLSCREEN_VIEWPORT_PATCH] = false;
531
+ }
532
+
533
+ function restoreToolMouseRenderPatch(): void {
534
+ if (toolMouseRenderPatchState) toolMouseRenderPatchState.active = false;
535
+ if (
536
+ toolMouseRenderPatchTui &&
537
+ toolMouseRenderPatchOriginal &&
538
+ toolMouseRenderPatchTui.doRender === toolMouseRenderPatchWrapper
539
+ ) {
540
+ toolMouseRenderPatchTui.doRender = toolMouseRenderPatchOriginal;
541
+ }
542
+ toolMouseRenderPatchTui = null;
543
+ toolMouseRenderPatchOriginal = null;
544
+ toolMouseRenderPatchWrapper = null;
545
+ toolMouseRenderPatchState = null;
546
+ toolMouseRawWrite = null;
547
+ latestInteractionFrame = { regions: [] };
548
+ }
549
+
550
+ function buildInteractionFrame(
551
+ tui: any,
552
+ renderedTools: FrameToolRender[],
553
+ placements: FrameToolPlacement[],
554
+ ): InteractionFrame {
555
+ const width = Math.max(1, Number(tui?.terminal?.columns) || 80);
556
+ // native: full buffer; map with the post-doRender previousViewportTop.
557
+ const lineIndexToScreenRow = (lineIndex: number) =>
558
+ lineIndex - (Number(tui?.previousViewportTop) || 0) + 1;
559
+ const visibleRows = Math.max(
560
+ 1,
561
+ Number(tui?.terminal?.rows) || Number.POSITIVE_INFINITY,
562
+ );
563
+ const regions: InteractionRegion[] = [];
564
+ const renderedByComponent = new Map<any, FrameToolRender>();
565
+ for (const rendered of renderedTools)
566
+ renderedByComponent.set(rendered.component, rendered);
567
+ const placementsByComponent = new Map<any, FrameToolPlacement[]>();
568
+ for (const placement of placements) {
569
+ const list = placementsByComponent.get(placement.component) ?? [];
570
+ list.push(placement);
571
+ placementsByComponent.set(placement.component, list);
572
+ }
573
+ for (const [component, componentPlacements] of placementsByComponent) {
574
+ const rendered = renderedByComponent.get(component);
575
+ if (!rendered) continue;
576
+ for (const placement of componentPlacements) {
577
+ const finalRow = lineIndexToScreenRow(placement.lineIndex);
578
+ if (finalRow < 1 || finalRow > visibleRows) continue;
579
+ // Hit columns come from the final painted line (parent may prefix/transform).
580
+ const line = placement.finalLine;
581
+ if (!component.expanded) {
582
+ const box = collapsedHintHitbox(line);
583
+ if (box && COLLAPSED_TOOL_SUMMARY.test(stripTerminalSequences(line))) {
584
+ regions.push({
585
+ kind: "collapsed-hint",
586
+ row: finalRow,
587
+ ...box,
588
+ component,
589
+ });
590
+ }
591
+ continue;
592
+ }
593
+ if (placement.view && placement.section) {
594
+ const plain = stripTerminalSequencesPreservingLayout(line);
595
+ const box = placement.view.showMoreHitbox(plain);
596
+ if (box) {
597
+ regions.push({
598
+ kind: "show-more",
599
+ row: finalRow,
600
+ ...box,
601
+ component,
602
+ view: placement.view,
603
+ section: placement.section,
604
+ });
605
+ }
606
+ }
607
+ }
608
+ if (!component.expanded) continue;
609
+ let cardStart = 0;
610
+ if (!(component instanceof ToolGroupComponent)) {
611
+ const box = component.contentBox;
612
+ if (
613
+ !box ||
614
+ !Array.isArray(component.children) ||
615
+ !component.children.includes(box)
616
+ ) {
617
+ continue;
618
+ }
619
+ if (!rendered.contentBoxLines) continue;
620
+ cardStart = Math.max(0, rendered.lines.length - rendered.contentBoxLines);
621
+ }
622
+ for (const placement of componentPlacements) {
623
+ if (placement.componentRow < cardStart) continue;
624
+ const finalRow = lineIndexToScreenRow(placement.lineIndex);
625
+ if (finalRow >= 1 && finalRow <= visibleRows) {
626
+ regions.push({
627
+ kind: "expanded-card",
628
+ row: finalRow,
629
+ startCol: 1,
630
+ endCol: width,
631
+ component,
632
+ });
633
+ }
634
+ }
635
+ }
636
+ return { regions };
637
+ }
638
+
639
+ /**
640
+ * 临时包装 outermost 工具/组件的 render 注入零宽 marker,返回待 restore 列表。
641
+ * 调用方必须用 restoreRenderOverride 立即还原(同一次渲染内有效)。
642
+ */
643
+ function wrapToolRendersForFrame(
644
+ outermost: any[],
645
+ renderedTools: FrameToolRender[],
646
+ idToComponent: Map<number, any>,
647
+ ): Array<{ target: any; descriptor?: PropertyDescriptor }> {
648
+ const restores: Array<{ target: any; descriptor?: PropertyDescriptor }> = [];
649
+ let nextId = 0;
650
+ try {
651
+ for (const component of outermost) {
652
+ const originalRender = component.render;
653
+ if (typeof originalRender !== "function") continue;
654
+ const id = nextId++;
655
+ idToComponent.set(id, component);
656
+ const wrappedRender = function (this: any, ...renderArgs: any[]) {
657
+ let contentBoxLines = 0;
658
+ const box = component.contentBox;
659
+ let boxRestore:
660
+ | { target: any; descriptor?: PropertyDescriptor }
661
+ | undefined;
662
+ if (
663
+ box &&
664
+ Array.isArray(component.children) &&
665
+ component.children.includes(box) &&
666
+ typeof box.render === "function"
667
+ ) {
668
+ const boxOriginal = box.render;
669
+ const boxWrapped = function (this: any, ...boxArgs: any[]) {
670
+ const boxLines = Reflect.apply(boxOriginal, this, boxArgs);
671
+ if (Array.isArray(boxLines)) contentBoxLines = boxLines.length;
672
+ return boxLines;
673
+ };
674
+ const boxDescriptor = defineRenderOverride(box, boxWrapped);
675
+ if (boxDescriptor !== undefined || box.render === boxWrapped) {
676
+ boxRestore = { target: box, descriptor: boxDescriptor };
677
+ }
678
+ }
679
+ try {
680
+ const lines = Reflect.apply(originalRender, this, renderArgs);
681
+ if (!Array.isArray(lines)) return lines;
682
+ renderedTools.push({
683
+ component,
684
+ lines: lines.map((line) => String(line)),
685
+ contentBoxLines,
686
+ });
687
+ return lines.map((line, row) => `${line}${toolFrameMarker(id, row)}`);
688
+ } finally {
689
+ if (boxRestore)
690
+ restoreRenderOverride(boxRestore.target, boxRestore.descriptor);
691
+ }
692
+ };
693
+ const descriptor = defineRenderOverride(component, wrappedRender);
694
+ if (descriptor !== undefined || component.render === wrappedRender) {
695
+ restores.push({ target: component, descriptor });
696
+ }
697
+ }
698
+ return restores;
699
+ } catch (error) {
700
+ for (const { target, descriptor } of restores.reverse()) {
701
+ restoreRenderOverride(target, descriptor);
702
+ }
703
+ throw error;
704
+ }
705
+ }
706
+
707
+ function defineRenderOverride(
708
+ target: any,
709
+ wrapped: (...args: any[]) => any,
710
+ ): PropertyDescriptor | undefined {
711
+ const descriptor = Object.getOwnPropertyDescriptor(target, "render");
712
+ try {
713
+ Object.defineProperty(
714
+ target,
715
+ "render",
716
+ descriptor && "value" in descriptor
717
+ ? { ...descriptor, value: wrapped }
718
+ : {
719
+ configurable: true,
720
+ enumerable: descriptor?.enumerable ?? false,
721
+ writable: true,
722
+ value: wrapped,
723
+ },
724
+ );
725
+ return descriptor;
726
+ } catch {
727
+ return undefined;
728
+ }
729
+ }
730
+
731
+ function restoreRenderOverride(
732
+ target: any,
733
+ descriptor: PropertyDescriptor | undefined,
734
+ ): void {
735
+ try {
736
+ if (descriptor) Object.defineProperty(target, "render", descriptor);
737
+ else delete target.render;
738
+ } catch {
739
+ // Keep restoring siblings after a hostile descriptor change.
740
+ }
741
+ }
742
+
743
+ function patchToolMouseMotionAfterRender(tui: any): void {
744
+ // Same tui is not enough: footer/compositor rebuild may replace doRender under us.
745
+ if (
746
+ toolMouseRenderPatchTui === tui &&
747
+ toolMouseRenderPatchState?.active &&
748
+ tui.doRender === toolMouseRenderPatchWrapper
749
+ ) {
750
+ return;
751
+ }
752
+ // 0.84+ 惰性 Proxy:捕获 doRender 会解析到 wrapper 自身(无限递归),跳过。
753
+ if (isLazyProxyTui(tui)) return;
754
+ restoreToolMouseRenderPatch();
755
+ const original = tui?.doRender;
756
+ const terminal = tui?.terminal;
757
+ const rawWrite =
758
+ typeof terminal?.write === "function" ? terminal.write : undefined;
759
+ if (typeof original !== "function") return;
760
+
761
+ toolMouseRawWrite = rawWrite
762
+ ? (data) => Reflect.apply(rawWrite, terminal, [data])
763
+ : null;
764
+ const patchState = { active: true };
765
+ const wrapper = function (this: any, ...args: any[]) {
766
+ if (!patchState.active) return Reflect.apply(original, this, args);
767
+ const renderedTools: FrameToolRender[] = [];
768
+ const idToComponent = new Map<number, any>();
769
+ const frame: IoViewFrameState = {
770
+ viewIds: new Map(),
771
+ idToView: new Map(),
772
+ nextId: 0,
773
+ };
774
+ const outermost: any[] = [];
775
+ collectToolComponents(this, outermost);
776
+ const restores = wrapToolRendersForFrame(
777
+ outermost,
778
+ renderedTools,
779
+ idToComponent,
780
+ );
781
+ let placements: FrameToolPlacement[] = [];
782
+ const originalTuiRender =
783
+ typeof this.render === "function" ? this.render : null;
784
+ let tuiRenderDescriptor: PropertyDescriptor | undefined;
785
+ let sawTuiRender = false;
786
+ if (originalTuiRender) {
787
+ const wrappedTuiRender = function (this: any, ...renderArgs: any[]) {
788
+ const lines = Reflect.apply(originalTuiRender, this, renderArgs);
789
+ if (!Array.isArray(lines)) return lines;
790
+ sawTuiRender = true;
791
+ const extracted = extractToolFramePlacements(
792
+ lines.map((line) => String(line)),
793
+ idToComponent,
794
+ frame.idToView,
795
+ );
796
+ placements = extracted.placements;
797
+ return extracted.lines;
798
+ };
799
+ tuiRenderDescriptor = defineRenderOverride(this, wrappedTuiRender);
800
+ }
801
+ let succeeded = false;
802
+ const previousFrame = getActiveIoViewFrame();
803
+ setActiveIoViewFrame(frame);
804
+ try {
805
+ const result = Reflect.apply(original, this, args);
806
+ succeeded = true;
807
+ // Test harnesses may paint via doRender without tui.render; recover markers there.
808
+ if (!sawTuiRender && Array.isArray(this.previousLines)) {
809
+ const extracted = extractToolFramePlacements(
810
+ this.previousLines.map((line: unknown) => String(line)),
811
+ idToComponent,
812
+ frame.idToView,
813
+ );
814
+ this.previousLines = extracted.lines;
815
+ placements = extracted.placements;
816
+ }
817
+ if (toolMouseInteractionActive())
818
+ toolMouseRawWrite?.(TOOL_MOUSE_MOTION_ENABLE);
819
+ return result;
820
+ } finally {
821
+ setActiveIoViewFrame(previousFrame);
822
+ if (originalTuiRender) restoreRenderOverride(this, tuiRenderDescriptor);
823
+ for (const { target, descriptor } of restores.reverse()) {
824
+ restoreRenderOverride(target, descriptor);
825
+ }
826
+ if (succeeded) {
827
+ latestInteractionFrame = buildInteractionFrame(
828
+ this,
829
+ renderedTools,
830
+ placements,
831
+ );
832
+ }
833
+ }
834
+ };
835
+ try {
836
+ tui.doRender = wrapper;
837
+ } catch {
838
+ toolMouseRawWrite = null;
839
+ return;
840
+ }
841
+ toolMouseRenderPatchTui = tui;
842
+ toolMouseRenderPatchOriginal = original;
843
+ toolMouseRenderPatchWrapper = wrapper;
844
+ toolMouseRenderPatchState = patchState;
845
+ if (toolMouseInteractionActive())
846
+ toolMouseRawWrite?.(TOOL_MOUSE_MOTION_ENABLE);
847
+ }
848
+
849
+ function handleToolMouseInput(data: string): { consume: true } | undefined {
850
+ if (!getToolMouseTui()) return undefined;
851
+ // 惰性 Proxy fullscreen:鼠标由 handleViewportInput 包装消费(官方链之前),
852
+ // 此处只处理键盘(鼠标事件在官方 listener 已被 consume,到不了这里)。
853
+ if (fullscreenLazyTui(getToolMouseTui())) {
854
+ scheduleScrollButtonSync(getToolMouseTui(), data);
855
+ if (isScrollBottomInput(data)) {
856
+ getToolMouseTui().scrollToBottom?.();
857
+ hideScrollButton(getToolMouseTui());
858
+ return { consume: true };
859
+ }
860
+ return undefined;
861
+ }
862
+ updateScrollButtonFromInput(getToolMouseTui(), data);
863
+ // Off mode restores native input: wheel keeps scrolling through Pi's normal
864
+ // dispatcher, while hover/click affordances are entirely inactive.
865
+ if (!toolMouseInteractionActive()) return undefined;
866
+ const packets = parseSgrMousePackets(data);
867
+ if (!packets) {
868
+ scheduleScrollButtonSync(getToolMouseTui(), data);
869
+ return undefined;
870
+ }
871
+
872
+ let consumed = false;
873
+ for (const packet of packets) {
874
+ updateToolSummaryHover(getToolMouseTui(), packet);
875
+ if (!isSgrLeftPress(packet)) continue;
876
+ if (toggleToolAtMouseClick(getToolMouseTui(), packet)) {
877
+ consumed = true;
878
+ }
879
+ }
880
+
881
+ // Let scrolling, motion, release, and clicks outside tool results reach the
882
+ // normal TUI input chain (including other extensions such as pi-zentui).
883
+ scheduleScrollButtonSync(getToolMouseTui(), data);
884
+ return consumed ? { consume: true } : undefined;
885
+ }
886
+
887
+ export function teardownToolMouseInteraction(
888
+ owner: object = toolMouseInstallationOwner ?? DEFAULT_TOOL_MOUSE_OWNER,
889
+ ): void {
890
+ const current = patchRegistry.get<object>(TOOL_MOUSE_OWNER_KEY);
891
+ if (current && current !== owner) return;
892
+ if (sessionRenderTimer) {
893
+ clearTimeout(sessionRenderTimer);
894
+ sessionRenderTimer = null;
895
+ }
896
+ toolMouseInputUnsubscribe?.();
897
+ toolMouseInputUnsubscribe = null;
898
+ setHoveredToolCallId(null);
899
+ setHoveredToolGroup(null);
900
+ setHoveredThinking(null);
901
+ setHoveredMessageDisplay(null);
902
+ setHoveredToolIo(null, null);
903
+ setHoveredCompactAssistant(null);
904
+ clearExpandPanelDoubleClick();
905
+ try {
906
+ if (isLazyProxyTui(getToolMouseTui()))
907
+ releaseFullscreenToolMouseMotion(getToolMouseTui());
908
+ else getToolMouseTui()?.terminal?.write?.(TOOL_MOUSE_DISABLE);
909
+ } catch {
910
+ // The terminal may already be closed during shutdown.
911
+ }
912
+ try {
913
+ toolMouseUi?.setWidget?.(TOOL_MOUSE_WIDGET_KEY, undefined);
914
+ } catch {
915
+ // The UI context may already have been reset during /reload.
916
+ }
917
+ restoreToolMouseRenderPatch();
918
+ restoreFullscreenViewportInput(getToolMouseTui());
919
+ resetScrollButtonState();
920
+ setToolMouseTui(null);
921
+ toolMouseUi = null;
922
+ patchRegistry.dispose(TOOL_MOUSE_OWNER_KEY, owner);
923
+ if (toolMouseInstallationOwner === owner) toolMouseInstallationOwner = null;
924
+ }
925
+
926
+ /** off 模式清理:清空 hover 与回到底部按钮状态(跨模块 rebind 统一经由此函数)。 */
927
+ export function resetToolHoverState(): void {
928
+ setHoveredToolCallId(null);
929
+ setHoveredThinking(null);
930
+ setHoveredMessageDisplay(null);
931
+ setHoveredCompactAssistant(null);
932
+ setScrollButtonVisible(false);
933
+ setScrollButtonHovered(false);
934
+ releaseFullscreenToolMouseMotion(getToolMouseTui());
935
+ }
936
+
937
+ export function installToolMouseInteraction(
938
+ ctx: any,
939
+ owner: object = DEFAULT_TOOL_MOUSE_OWNER,
940
+ ): void {
941
+ teardownToolMouseInteraction(toolMouseInstallationOwner ?? owner);
942
+ if (ctx?.mode !== "tui" || !ctx?.hasUI) return;
943
+ if (
944
+ typeof ctx.ui?.onTerminalInput !== "function" ||
945
+ typeof ctx.ui?.setWidget !== "function"
946
+ )
947
+ return;
948
+
949
+ toolMouseInstallationOwner = owner;
950
+ patchRegistry.install(TOOL_MOUSE_OWNER_KEY, owner);
951
+ setHoveredToolCallId(null);
952
+ toolMouseUi = ctx.ui;
953
+ // 0.84+ 的 tui 是惰性 Proxy:regular 保留原生 scrollback;fullscreen
954
+ // 由官方 LayoutFrame 命中,并由扩展补齐 hover 所需的 all-motion 上报。
955
+ ctx.ui.setWidget(TOOL_MOUSE_WIDGET_KEY, (tui: any, theme: any) => {
956
+ setToolMouseTui(tui);
957
+ setToolTuiFullscreen(fullscreenLazyTui(tui));
958
+ if (isLazyProxyTui(tui)) {
959
+ patchFullscreenViewportInput(tui);
960
+ ensureFullscreenToolMouseMotion(tui);
961
+ setScrollButtonWidget({
962
+ render: (width: number) => {
963
+ patchFullscreenViewportInput(tui);
964
+ ensureFullscreenToolMouseMotion(tui);
965
+ return renderScrollButton(width, theme);
966
+ },
967
+ invalidate() {},
968
+ });
969
+ return getScrollButtonWidget();
970
+ }
971
+ // Wrap doRender to capture the live frame for tool click/hover mapping.
972
+ patchToolMouseMotionAfterRender(tui);
973
+ if (toolMouseInteractionActive())
974
+ tui?.terminal?.write?.(TOOL_MOUSE_MOTION_ENABLE);
975
+ const widget = {
976
+ render: (width: number) => renderScrollButton(width, theme),
977
+ invalidate() {},
978
+ };
979
+ setScrollButtonWidget(widget);
980
+ return widget;
981
+ });
982
+ toolMouseInputUnsubscribe = ctx.ui.onTerminalInput(handleToolMouseInput);
983
+ }
984
+
985
+ function refreshToolRendererComponents(tui: any): void {
986
+ const tools: any[] = [];
987
+ collectToolComponents(tui, tools);
988
+ for (const tool of tools) tool.invalidate?.();
989
+ }
990
+
991
+ export function scheduleSessionRender(refresh?: () => void): void {
992
+ const tui = getToolMouseTui();
993
+ if (!tui || typeof tui.requestRender !== "function") return;
994
+ if (sessionRenderTimer) clearTimeout(sessionRenderTimer);
995
+ // Restored transcripts are populated at different points for startup, reload,
996
+ // and session replacement. Repaint after session_start and the surrounding UI
997
+ // rebuild finish so messages are not left hidden until the next terminal input.
998
+ sessionRenderTimer = setTimeout(() => {
999
+ sessionRenderTimer = null;
1000
+ if (getToolMouseTui() !== tui) return;
1001
+ patchToolMouseMotionAfterRender(tui);
1002
+ refreshToolRendererComponents(tui);
1003
+ refresh?.();
1004
+ tui.requestRender(true);
1005
+ }, 0);
1006
+ }
1007
+
1008
+ // toolMouseTui 跨模块读取一律走 getToolMouseTui()(jiti 转译下 let 绑定可能是快照);
1009
+ // 此 re-export 仅保留兼容旧导入,新代码请用 getToolMouseTui。
1010
+ export { toolMouseTui } from "./scroll.ts";
1011
+ export {
1012
+ hoveredToolCallId,
1013
+ isToolCallHovered,
1014
+ setHoveredToolGroup,
1015
+ setHoveredToolIo,
1016
+ } from "./hover.ts";