pi-one-ui 0.7.2 → 0.9.0
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.
- package/CHANGELOG.md +52 -1
- package/README.en.md +5 -8
- package/README.md +6 -7
- package/extensions/app/config/footer.ts +21 -5
- package/extensions/app/config/renderer.ts +1 -12
- package/extensions/app/config/shell.ts +62 -60
- package/extensions/app/runtime/prototype-patch-registry.ts +13 -9
- package/extensions/app/runtime/tui-runtime.ts +19 -12
- package/extensions/app/settings/controller.ts +2 -2
- package/extensions/layouts/context/index.ts +2 -2
- package/extensions/layouts/context/message/user-message-osc.ts +1 -1
- package/extensions/layouts/context/message/user-message-styles.ts +9 -6
- package/extensions/layouts/context/message/user-message.ts +5 -5
- package/extensions/layouts/context/renderer/compact-mode.ts +2 -2
- package/extensions/layouts/context/renderer/default-mode.ts +10 -10
- package/extensions/layouts/context/renderer/index.ts +0 -1
- package/extensions/layouts/context/renderer/mouse/hover.ts +0 -4
- package/extensions/layouts/context/renderer/mouse/interaction.ts +106 -95
- package/extensions/layouts/context/renderer/mouse/layout.ts +0 -4
- package/extensions/layouts/context/renderer/mouse/scroll.ts +2 -183
- package/extensions/layouts/context/renderer/tool/diff/diff-edit-render.ts +1 -1
- package/extensions/layouts/context/renderer/tool/diff/index.ts +1 -1
- package/extensions/layouts/context/renderer/tool/grouping.ts +3 -3
- package/extensions/layouts/context/renderer/tool/message-display.ts +7 -7
- package/extensions/layouts/context/renderer/tool/result.ts +16 -16
- package/extensions/layouts/context/renderer/tool/toggle-render-cache.ts +9 -9
- package/extensions/layouts/editor/controller.ts +17 -17
- package/extensions/layouts/editor/editor-metadata-format.ts +5 -5
- package/extensions/layouts/editor/factory.ts +5 -5
- package/extensions/layouts/editor/minimalist-editor.ts +8 -8
- package/extensions/layouts/editor/ownership.ts +24 -20
- package/extensions/layouts/editor/ui.ts +10 -15
- package/extensions/layouts/footer/controller.ts +4 -4
- package/extensions/layouts/footer/data.ts +2 -2
- package/extensions/layouts/footer/extension-status.ts +4 -4
- package/extensions/layouts/footer/footer.ts +5 -2
- package/extensions/layouts/overlay/selector-border.ts +4 -4
- package/extensions/layouts/overlay/selector-controller.ts +1 -1
- package/extensions/layouts/overlay/settings-previews.ts +2 -2
- package/extensions/layouts/working-line/interaction-summary.ts +3 -1
- package/extensions/layouts/working-line/working-line.ts +4 -4
- package/extensions/services/package-data.ts +1 -1
- package/extensions/shared/style.ts +66 -56
- package/extensions/tools/patch-keys.ts +22 -25
- package/package.json +7 -7
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getKeybindings } from "@earendil-works/pi-tui";
|
|
2
2
|
import {
|
|
3
3
|
type InputRouter,
|
|
4
4
|
inputRouter,
|
|
@@ -63,20 +63,9 @@ import {
|
|
|
63
63
|
} from "./packets.ts";
|
|
64
64
|
import {
|
|
65
65
|
fullscreenLazyTui,
|
|
66
|
-
getScrollButtonVisible,
|
|
67
|
-
getScrollButtonWidget,
|
|
68
66
|
getToolMouseTui,
|
|
69
|
-
hideScrollButton,
|
|
70
|
-
isScrollBottomInput,
|
|
71
|
-
renderScrollButton,
|
|
72
|
-
resetScrollButtonState,
|
|
73
|
-
scheduleScrollButtonSync,
|
|
74
|
-
setScrollButtonHovered,
|
|
75
|
-
setScrollButtonVisible,
|
|
76
|
-
setScrollButtonWidget,
|
|
77
67
|
setToolMouseTui,
|
|
78
68
|
toolMouseInteractionActive,
|
|
79
|
-
updateScrollButtonFromInput,
|
|
80
69
|
} from "./scroll.ts";
|
|
81
70
|
|
|
82
71
|
type FrameToolRender = {
|
|
@@ -86,7 +75,7 @@ type FrameToolRender = {
|
|
|
86
75
|
};
|
|
87
76
|
|
|
88
77
|
type InteractionRegion = {
|
|
89
|
-
kind: "collapsed-hint" | "expanded-card" | "show-more"
|
|
78
|
+
kind: "collapsed-hint" | "expanded-card" | "show-more";
|
|
90
79
|
row: number;
|
|
91
80
|
startCol: number;
|
|
92
81
|
endCol: number;
|
|
@@ -97,10 +86,15 @@ type InteractionRegion = {
|
|
|
97
86
|
|
|
98
87
|
type InteractionFrame = { regions: InteractionRegion[] };
|
|
99
88
|
|
|
100
|
-
const TOOL_MOUSE_WIDGET_KEY = "
|
|
89
|
+
const TOOL_MOUSE_WIDGET_KEY = "oneui-tool-mouse";
|
|
101
90
|
const TOOL_MOUSE_MOTION_ENABLE = "\x1b[?1003h\x1b[?1006h";
|
|
102
91
|
const TOOL_MOUSE_MOTION_DISABLE = "\x1b[?1003l";
|
|
103
|
-
const FULLSCREEN_MOTION_ENABLED = Symbol("
|
|
92
|
+
const FULLSCREEN_MOTION_ENABLED = Symbol("pi-one-ui.fullscreen-motion-enabled");
|
|
93
|
+
const FULLSCREEN_SCROLL_END_STYLE_PATCH = Symbol(
|
|
94
|
+
"pi-one-ui.fullscreen-scroll-end-style-patch",
|
|
95
|
+
);
|
|
96
|
+
/** 指示条 hover 状态:样式函数与命中检测同模块,模块级绑定即可。 */
|
|
97
|
+
let scrollEndIndicatorHovered = false;
|
|
104
98
|
const DEFAULT_TOOL_MOUSE_OWNER = {};
|
|
105
99
|
export const TOOL_MOUSE_DISABLE = "\x1b[?1006l\x1b[?1003l\x1b[?1000l";
|
|
106
100
|
|
|
@@ -118,7 +112,7 @@ let ownsFullscreenMotion = false;
|
|
|
118
112
|
let sessionRenderTimer: ReturnType<typeof setTimeout> | null = null;
|
|
119
113
|
let latestInteractionFrame: InteractionFrame = { regions: [] };
|
|
120
114
|
|
|
121
|
-
/** Summary markers used by Pi and
|
|
115
|
+
/** Summary markers used by Pi and pi-one-ui; unlike the trailing hint, these survive truncation. */
|
|
122
116
|
const COLLAPSED_TOOL_SUMMARY = /^\s*(?:↳|└|⎿|●|✓|✗|…)/;
|
|
123
117
|
|
|
124
118
|
function interactionRegionAt(packet: SgrMousePacket): InteractionRegion | null {
|
|
@@ -130,7 +124,6 @@ function interactionRegionAt(packet: SgrMousePacket): InteractionRegion | null {
|
|
|
130
124
|
);
|
|
131
125
|
return (
|
|
132
126
|
matches.find((region) => region.kind === "show-more") ??
|
|
133
|
-
matches.find((region) => region.kind === "scroll-bottom") ??
|
|
134
127
|
matches.find((region) => region.kind === "collapsed-hint") ??
|
|
135
128
|
matches.find((region) => region.kind === "expanded-card") ??
|
|
136
129
|
null
|
|
@@ -156,8 +149,6 @@ function tryOpenToolIoShowMore(region: InteractionRegion): boolean {
|
|
|
156
149
|
function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
|
|
157
150
|
if ((packet.code & 32) === 0 || packet.final !== "M") return;
|
|
158
151
|
const region = interactionRegionAt(packet);
|
|
159
|
-
const nextScrollButtonHovered = region?.kind === "scroll-bottom";
|
|
160
|
-
const scrollButtonChanged = setScrollButtonHovered(nextScrollButtonHovered);
|
|
161
152
|
const component = region?.component;
|
|
162
153
|
const nextToolCallId =
|
|
163
154
|
region?.kind === "collapsed-hint" ? (component?.toolCallId ?? null) : null;
|
|
@@ -169,7 +160,6 @@ function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
|
|
|
169
160
|
const changed = nextToolCallId !== sharedToolHoverState().toolCallId;
|
|
170
161
|
setHoveredToolCallId(nextToolCallId);
|
|
171
162
|
if (
|
|
172
|
-
scrollButtonChanged ||
|
|
173
163
|
setHoveredToolIo(nextIoView, nextIoSection) ||
|
|
174
164
|
setHoveredToolGroup(nextGroup) ||
|
|
175
165
|
changed
|
|
@@ -218,7 +208,6 @@ function collapseExpandedCard(tui: any, card: any): boolean {
|
|
|
218
208
|
function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
|
|
219
209
|
const region = interactionRegionAt(packet);
|
|
220
210
|
if (!region) return false;
|
|
221
|
-
if (region.kind === "scroll-bottom") return false;
|
|
222
211
|
if (region.kind === "show-more") return tryOpenToolIoShowMore(region);
|
|
223
212
|
const component = region.component;
|
|
224
213
|
if (!component) return false;
|
|
@@ -255,13 +244,6 @@ function ensureFullscreenToolMouseMotion(tui: any): void {
|
|
|
255
244
|
releaseFullscreenToolMouseMotion(tui);
|
|
256
245
|
return;
|
|
257
246
|
}
|
|
258
|
-
// 面板改 scrollStepLines 后,下一帧渲染即同步(restore 仍按 original 恢复)。
|
|
259
|
-
if (
|
|
260
|
-
typeof tui.wheelScrollLines === "number" &&
|
|
261
|
-
tui.wheelScrollLines !== config.scrollStepLines
|
|
262
|
-
) {
|
|
263
|
-
tui.wheelScrollLines = config.scrollStepLines;
|
|
264
|
-
}
|
|
265
247
|
if (
|
|
266
248
|
!toolMouseInteractionActive() ||
|
|
267
249
|
tui.mouseEnabled === false ||
|
|
@@ -298,15 +280,12 @@ function releaseFullscreenToolMouseMotion(tui?: any): void {
|
|
|
298
280
|
}
|
|
299
281
|
}
|
|
300
282
|
|
|
301
|
-
const FULLSCREEN_VIEWPORT_PATCH = Symbol("
|
|
302
|
-
const FULLSCREEN_WHEEL_SCROLL_ORIGINAL = Symbol(
|
|
303
|
-
"ccstyle.fullscreen-wheel-scroll-original",
|
|
304
|
-
);
|
|
283
|
+
const FULLSCREEN_VIEWPORT_PATCH = Symbol("pi-one-ui.fullscreen-viewport-patch");
|
|
305
284
|
|
|
306
285
|
/**
|
|
307
286
|
* 官方 fullscreen 工具卡点击:collapsed hint 单击展开
|
|
308
287
|
* (有且仅保持一个展开:展开前收起其他工具卡),expanded 整卡双击收起,
|
|
309
|
-
* 截断头 show-more
|
|
288
|
+
* 截断头 show-more 单击打开全量预览。
|
|
310
289
|
* 滚动条列、含 OSC8 链接行、非工具区域、展开卡单击放行官方。
|
|
311
290
|
*/
|
|
312
291
|
function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
@@ -316,6 +295,8 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
316
295
|
const x = packet.col - 1;
|
|
317
296
|
const y = packet.row - 1;
|
|
318
297
|
if (isScrollbarColumnAt(layout, x)) return false;
|
|
298
|
+
// 指示条覆盖层优先:命中区放行官方,由官方 rect 命中触发 scrollToBottom。
|
|
299
|
+
if (isScrollEndIndicatorAt(tui, x, y)) return false;
|
|
319
300
|
const hit = fullscreenLeafAt(layout, x, y);
|
|
320
301
|
if (!hit) return false;
|
|
321
302
|
const width = Math.max(1, Number(tui.terminal?.columns) || 80);
|
|
@@ -330,12 +311,6 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
330
311
|
if (!target) return false;
|
|
331
312
|
const component = target.component;
|
|
332
313
|
const card = target.group ?? component;
|
|
333
|
-
// 回到底部按钮:按组件引用命中,不依赖渲染行缓存。
|
|
334
|
-
if (getScrollButtonVisible() && component === getScrollButtonWidget()) {
|
|
335
|
-
tui.scrollToBottom?.();
|
|
336
|
-
hideScrollButton(tui);
|
|
337
|
-
return true;
|
|
338
|
-
}
|
|
339
314
|
const line = hit.box.lines?.[hit.localRow];
|
|
340
315
|
if (typeof line !== "string" || /\x1b]8;[^;]*;/.test(line)) return false;
|
|
341
316
|
const isTool = isToolExecutionComponent(component);
|
|
@@ -401,30 +376,96 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
401
376
|
|
|
402
377
|
/**
|
|
403
378
|
* fullscreen 鼠标悬停:collapsed 卡 [click to show more] hint、
|
|
404
|
-
* expanded 卡截断头 show-more
|
|
379
|
+
* expanded 卡截断头 show-more、回到底部指示条。motion 不 consume,官方链照常。
|
|
405
380
|
*/
|
|
381
|
+
function formatKeyDisplay(keys: string[]): string {
|
|
382
|
+
return keys
|
|
383
|
+
.join("/")
|
|
384
|
+
.split("/")
|
|
385
|
+
.map((part) =>
|
|
386
|
+
part
|
|
387
|
+
.split("+")
|
|
388
|
+
.map((piece) =>
|
|
389
|
+
piece.length <= 1
|
|
390
|
+
? piece.toUpperCase()
|
|
391
|
+
: `${piece[0]?.toUpperCase() ?? ""}${piece.slice(1)}`,
|
|
392
|
+
)
|
|
393
|
+
.join("+"),
|
|
394
|
+
)
|
|
395
|
+
.join("/");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** 回到底部快捷键(tui.altScreen.bottom)显示文本;未配置时省略快捷键段。 */
|
|
399
|
+
function scrollEndShortcutText(): string {
|
|
400
|
+
const keys = getKeybindings().getKeys("tui.altScreen.bottom");
|
|
401
|
+
return keys.length > 0 ? formatKeyDisplay(keys) : "";
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** pi-one-ui 按钮样式:[ ↓ Back to bottom · <key> ],accent 色、hover 转 text 色。 */
|
|
405
|
+
function renderScrollEndIndicatorLabel(theme: any): string {
|
|
406
|
+
const shortcut = scrollEndShortcutText();
|
|
407
|
+
const label = `[ ↓ Back to bottom${shortcut ? ` · ${shortcut}` : ""} ]`;
|
|
408
|
+
return theme.fg(scrollEndIndicatorHovered ? "text" : "accent", label);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** 指示条是 composite 层覆盖物(不在布局树内),按官方 rect 判定命中。 */
|
|
412
|
+
function isScrollEndIndicatorAt(tui: any, x: number, y: number): boolean {
|
|
413
|
+
const rect = tui.scrollToEndIndicatorRect;
|
|
414
|
+
return Boolean(
|
|
415
|
+
rect && y === rect.row && x >= rect.column && x < rect.column + rect.width,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** 指示条 hover 同步;返回是否悬停在指示条上(命中时优先于下方组件)。 */
|
|
420
|
+
function updateScrollEndIndicatorHover(
|
|
421
|
+
tui: any,
|
|
422
|
+
x: number,
|
|
423
|
+
y: number,
|
|
424
|
+
): boolean {
|
|
425
|
+
const hovered = isScrollEndIndicatorAt(tui, x, y);
|
|
426
|
+
if (hovered !== scrollEndIndicatorHovered) {
|
|
427
|
+
scrollEndIndicatorHovered = hovered;
|
|
428
|
+
tui.requestRender?.();
|
|
429
|
+
}
|
|
430
|
+
return hovered;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* 覆盖官方 scrollToEndIndicator 样式函数(实例字段,官方每帧调用取标签文本;
|
|
435
|
+
* 显示条件、居中、点击命中、scrollToBottom 均保留官方机制)。惰性 Proxy 的
|
|
436
|
+
* get 会包装函数属性,保存到 Symbol 的是等价转发包装,恢复后行为不变。
|
|
437
|
+
*/
|
|
438
|
+
function installScrollEndIndicatorStyle(tui: any, theme: any): void {
|
|
439
|
+
if (tui[FULLSCREEN_SCROLL_END_STYLE_PATCH]) return;
|
|
440
|
+
const original = tui.scrollToEndIndicator;
|
|
441
|
+
if (typeof original !== "function") return;
|
|
442
|
+
tui[FULLSCREEN_SCROLL_END_STYLE_PATCH] = original;
|
|
443
|
+
tui.scrollToEndIndicator = () => renderScrollEndIndicatorLabel(theme);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function restoreScrollEndIndicatorStyle(tui: any): void {
|
|
447
|
+
const original = tui?.[FULLSCREEN_SCROLL_END_STYLE_PATCH];
|
|
448
|
+
if (typeof original !== "function") return;
|
|
449
|
+
tui.scrollToEndIndicator = original;
|
|
450
|
+
tui[FULLSCREEN_SCROLL_END_STYLE_PATCH] = undefined;
|
|
451
|
+
scrollEndIndicatorHovered = false;
|
|
452
|
+
}
|
|
453
|
+
|
|
406
454
|
function handleFullscreenToolHover(tui: any, packet: SgrMousePacket): void {
|
|
407
455
|
if (packet.final !== "M") return;
|
|
408
456
|
const layout = tui.currentLayout;
|
|
409
457
|
if (!layout?.root) return;
|
|
410
458
|
const x = packet.col - 1;
|
|
411
459
|
const y = packet.row - 1;
|
|
460
|
+
if (updateScrollEndIndicatorHover(tui, x, y)) {
|
|
461
|
+
applyFullscreenHover(tui, null);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
412
464
|
let target: FullscreenHoverTarget | null = null;
|
|
413
465
|
const hit = fullscreenLeafAt(layout, x, y);
|
|
414
466
|
if (hit) {
|
|
415
467
|
const line = hit.box.lines?.[hit.localRow];
|
|
416
|
-
|
|
417
|
-
if (
|
|
418
|
-
typeof line === "string" &&
|
|
419
|
-
getScrollButtonVisible() &&
|
|
420
|
-
line.includes("[ ↓")
|
|
421
|
-
) {
|
|
422
|
-
const plain = stripTerminalSequencesPreservingLayout(line);
|
|
423
|
-
const idx = plain.indexOf("[ ↓");
|
|
424
|
-
if (idx >= 0 && x >= idx && x <= idx + plain.length - 1) {
|
|
425
|
-
target = { kind: "button" };
|
|
426
|
-
}
|
|
427
|
-
} else if (typeof line === "string" && !/\x1b]8;/.test(line)) {
|
|
468
|
+
if (typeof line === "string" && !/\x1b]8;/.test(line)) {
|
|
428
469
|
const width = Math.max(1, Number(tui.terminal?.columns) || 80);
|
|
429
470
|
const contentWidth = fullscreenContentWidth(hit.box, width);
|
|
430
471
|
// 与点击共用同一定位算法,避免 hover 自建行段与真实组件树错位。
|
|
@@ -489,17 +530,9 @@ function patchFullscreenViewportInput(tui: any): void {
|
|
|
489
530
|
const proto = Object.getPrototypeOf(tui);
|
|
490
531
|
const original = proto?.handleViewportInput;
|
|
491
532
|
if (typeof original !== "function") return;
|
|
492
|
-
// 官方原生 routeWheel 已完整处理嵌套 ScrollView;只调整默认步进(config.scrollStepLines)。
|
|
493
|
-
if (typeof tui.wheelScrollLines === "number") {
|
|
494
|
-
tui[FULLSCREEN_WHEEL_SCROLL_ORIGINAL] = tui.wheelScrollLines;
|
|
495
|
-
tui.wheelScrollLines = config.scrollStepLines;
|
|
496
|
-
}
|
|
497
533
|
tui[FULLSCREEN_VIEWPORT_PATCH] = true;
|
|
498
534
|
tui.handleViewportInput = function (this: any, data: string) {
|
|
499
535
|
if (toolMouseInteractionActive() && tui.mode === "fullscreen") {
|
|
500
|
-
// 滚动输入(wheel/pageUp/end 等)后同步回到底部按钮显隐;
|
|
501
|
-
// 官方 viewport 会消费键盘,扩展监听器无法补偿,必须在这里调度。
|
|
502
|
-
scheduleScrollButtonSync(tui, data);
|
|
503
536
|
const packets = parseSgrMousePackets(data);
|
|
504
537
|
// 官方 fullscreen 会消费全部鼠标;文本预览 overlay 活动时放行给 focused
|
|
505
538
|
// custom component,使 [esc] 点击和滚轮可用。
|
|
@@ -529,11 +562,6 @@ function restoreFullscreenViewportInput(tui: any): void {
|
|
|
529
562
|
if (typeof proto?.handleViewportInput === "function") {
|
|
530
563
|
tui.handleViewportInput = proto.handleViewportInput;
|
|
531
564
|
}
|
|
532
|
-
const originalWheelScrollLines = tui[FULLSCREEN_WHEEL_SCROLL_ORIGINAL];
|
|
533
|
-
if (typeof originalWheelScrollLines === "number") {
|
|
534
|
-
tui.wheelScrollLines = originalWheelScrollLines;
|
|
535
|
-
tui[FULLSCREEN_WHEEL_SCROLL_ORIGINAL] = undefined;
|
|
536
|
-
}
|
|
537
565
|
tui[FULLSCREEN_VIEWPORT_PATCH] = false;
|
|
538
566
|
}
|
|
539
567
|
|
|
@@ -857,24 +885,12 @@ function handleToolMouseInput(data: string): { consume: true } | undefined {
|
|
|
857
885
|
if (!getToolMouseTui()) return undefined;
|
|
858
886
|
// 惰性 Proxy fullscreen:鼠标由 handleViewportInput 包装消费(官方链之前),
|
|
859
887
|
// 此处只处理键盘(鼠标事件在官方 listener 已被 consume,到不了这里)。
|
|
860
|
-
if (fullscreenLazyTui(getToolMouseTui()))
|
|
861
|
-
scheduleScrollButtonSync(getToolMouseTui(), data);
|
|
862
|
-
if (isScrollBottomInput(data)) {
|
|
863
|
-
getToolMouseTui().scrollToBottom?.();
|
|
864
|
-
hideScrollButton(getToolMouseTui());
|
|
865
|
-
return { consume: true };
|
|
866
|
-
}
|
|
867
|
-
return undefined;
|
|
868
|
-
}
|
|
869
|
-
updateScrollButtonFromInput(getToolMouseTui(), data);
|
|
888
|
+
if (fullscreenLazyTui(getToolMouseTui())) return undefined;
|
|
870
889
|
// Off mode restores native input: wheel keeps scrolling through Pi's normal
|
|
871
890
|
// dispatcher, while hover/click affordances are entirely inactive.
|
|
872
891
|
if (!toolMouseInteractionActive()) return undefined;
|
|
873
892
|
const packets = parseSgrMousePackets(data);
|
|
874
|
-
if (!packets)
|
|
875
|
-
scheduleScrollButtonSync(getToolMouseTui(), data);
|
|
876
|
-
return undefined;
|
|
877
|
-
}
|
|
893
|
+
if (!packets) return undefined;
|
|
878
894
|
|
|
879
895
|
let consumed = false;
|
|
880
896
|
for (const packet of packets) {
|
|
@@ -887,7 +903,6 @@ function handleToolMouseInput(data: string): { consume: true } | undefined {
|
|
|
887
903
|
|
|
888
904
|
// Let scrolling, motion, release, and clicks outside tool results reach the
|
|
889
905
|
// normal TUI input chain (including other extensions such as pi-zentui).
|
|
890
|
-
scheduleScrollButtonSync(getToolMouseTui(), data);
|
|
891
906
|
return consumed ? { consume: true } : undefined;
|
|
892
907
|
}
|
|
893
908
|
|
|
@@ -923,21 +938,19 @@ export function teardownToolMouseInteraction(
|
|
|
923
938
|
}
|
|
924
939
|
restoreToolMouseRenderPatch();
|
|
925
940
|
restoreFullscreenViewportInput(getToolMouseTui());
|
|
926
|
-
|
|
941
|
+
restoreScrollEndIndicatorStyle(getToolMouseTui());
|
|
927
942
|
setToolMouseTui(null);
|
|
928
943
|
toolMouseUi = null;
|
|
929
944
|
patchRegistry.dispose(TOOL_MOUSE_OWNER_KEY, owner);
|
|
930
945
|
if (toolMouseInstallationOwner === owner) toolMouseInstallationOwner = null;
|
|
931
946
|
}
|
|
932
947
|
|
|
933
|
-
/** off 模式清理:清空 hover
|
|
948
|
+
/** off 模式清理:清空 hover 状态(跨模块 rebind 统一经由此函数)。 */
|
|
934
949
|
export function resetToolHoverState(): void {
|
|
935
950
|
setHoveredToolCallId(null);
|
|
936
951
|
setHoveredThinking(null);
|
|
937
952
|
setHoveredMessageDisplay(null);
|
|
938
953
|
setHoveredCompactAssistant(null);
|
|
939
|
-
setScrollButtonVisible(false);
|
|
940
|
-
setScrollButtonHovered(false);
|
|
941
954
|
releaseFullscreenToolMouseMotion(getToolMouseTui());
|
|
942
955
|
}
|
|
943
956
|
|
|
@@ -983,26 +996,24 @@ export function installToolMouseInteraction(
|
|
|
983
996
|
if (isLazyProxyTui(tui)) {
|
|
984
997
|
patchFullscreenViewportInput(tui);
|
|
985
998
|
ensureFullscreenToolMouseMotion(tui);
|
|
986
|
-
|
|
987
|
-
|
|
999
|
+
installScrollEndIndicatorStyle(tui, theme);
|
|
1000
|
+
return {
|
|
1001
|
+
render: () => {
|
|
1002
|
+
// 惰性 Proxy 下 renderer 可能被替换,每帧补装 viewport 包装与 motion。
|
|
988
1003
|
patchFullscreenViewportInput(tui);
|
|
989
1004
|
ensureFullscreenToolMouseMotion(tui);
|
|
990
|
-
|
|
1005
|
+
installScrollEndIndicatorStyle(tui, theme);
|
|
1006
|
+
return [];
|
|
991
1007
|
},
|
|
992
1008
|
invalidate() {},
|
|
993
|
-
}
|
|
994
|
-
return getScrollButtonWidget();
|
|
1009
|
+
};
|
|
995
1010
|
}
|
|
996
1011
|
// Wrap doRender to capture the live frame for tool click/hover mapping.
|
|
997
1012
|
patchToolMouseMotionAfterRender(tui);
|
|
1013
|
+
installScrollEndIndicatorStyle(tui, theme);
|
|
998
1014
|
if (toolMouseInteractionActive())
|
|
999
1015
|
tui?.terminal?.write?.(TOOL_MOUSE_MOTION_ENABLE);
|
|
1000
|
-
|
|
1001
|
-
render: (width: number) => renderScrollButton(width, theme),
|
|
1002
|
-
invalidate() {},
|
|
1003
|
-
};
|
|
1004
|
-
setScrollButtonWidget(widget);
|
|
1005
|
-
return widget;
|
|
1016
|
+
return { render: () => [], invalidate() {} };
|
|
1006
1017
|
});
|
|
1007
1018
|
const removeInput = services.input.bind(
|
|
1008
1019
|
ctx.ui,
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
isToolExecutionComponent,
|
|
8
8
|
stripTerminalSequencesPreservingLayout,
|
|
9
9
|
} from "./packets.ts";
|
|
10
|
-
import { getScrollButtonWidget } from "./scroll.ts";
|
|
11
10
|
|
|
12
11
|
export type ComponentRowHit = {
|
|
13
12
|
component: any;
|
|
@@ -156,9 +155,6 @@ export function componentAtLocalRow(
|
|
|
156
155
|
if (isMessageDisplayComponent(component)) {
|
|
157
156
|
return { component, row: localRow };
|
|
158
157
|
}
|
|
159
|
-
if (component === getScrollButtonWidget()) {
|
|
160
|
-
return { component, row: localRow };
|
|
161
|
-
}
|
|
162
158
|
if (!Array.isArray(component.children)) return null;
|
|
163
159
|
let offset = 0;
|
|
164
160
|
for (const child of component.children) {
|
|
@@ -1,28 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getKeybindings,
|
|
3
|
-
type Keybinding,
|
|
4
|
-
matchesKey,
|
|
5
|
-
truncateToWidth,
|
|
6
|
-
visibleWidth,
|
|
7
|
-
} from "@earendil-works/pi-tui";
|
|
8
1
|
import { config } from "../../../../app/config/renderer.ts";
|
|
9
2
|
import { isLazyProxyTui } from "../../../../tools/fullscreen-detect.ts";
|
|
10
3
|
import {
|
|
11
4
|
patchRegistry,
|
|
12
|
-
SCROLL_BUTTON_STATE_SLOT,
|
|
13
5
|
TOOL_MOUSE_TUI_SLOT,
|
|
14
6
|
} from "../../../../tools/patch-keys.ts";
|
|
15
|
-
import { parseSgrMousePackets } from "./packets.ts";
|
|
16
|
-
|
|
17
|
-
const ZENTUI_PAGE_UP_INPUT =
|
|
18
|
-
/^\x1b\[5;9(?::[12])?~$|^\x1b\[57421;9(?::[12])?u$|^\x1b\[1;6A$/;
|
|
19
|
-
const ZENTUI_PAGE_DOWN_INPUT =
|
|
20
|
-
/^\x1b\[6;9(?::[12])?~$|^\x1b\[57422;9(?::[12])?u$|^\x1b\[1;6B$/;
|
|
21
|
-
const SCROLL_BOTTOM_SHORTCUT = "ctrl+end";
|
|
22
7
|
|
|
23
8
|
/**
|
|
24
|
-
* 当前安装的 tui
|
|
25
|
-
*
|
|
9
|
+
* 当前安装的 tui 宿主。由 mouse-interaction 经 setToolMouseTui 维护;
|
|
10
|
+
* 跨模块一律经绑定/setter 访问。
|
|
26
11
|
*
|
|
27
12
|
* 状态镜像到 globalThis(Symbol 槽):jiti 转译下经 re-export 链读取的
|
|
28
13
|
* 模块级 let 绑定是初始值快照(死绑定,实测恒 null),函数调用才是活引用。
|
|
@@ -38,50 +23,6 @@ export function setToolMouseTui(tui: any): void {
|
|
|
38
23
|
patchRegistry.install(TOOL_MOUSE_TUI_SLOT, tui);
|
|
39
24
|
}
|
|
40
25
|
|
|
41
|
-
// 滚动按钮状态同 toolMouseTui:镜像到 globalThis(Symbol 槽),跨模块读取
|
|
42
|
-
// 一律用 getter(jiti 转译下模块级 let 绑定是初始值快照)。
|
|
43
|
-
type ScrollButtonState = { visible: boolean; hovered: boolean; widget: any };
|
|
44
|
-
function scrollButtonState(): ScrollButtonState {
|
|
45
|
-
return patchRegistry.ensure(SCROLL_BUTTON_STATE_SLOT, () => ({
|
|
46
|
-
visible: false,
|
|
47
|
-
hovered: false,
|
|
48
|
-
widget: null,
|
|
49
|
-
}));
|
|
50
|
-
}
|
|
51
|
-
export function getScrollButtonVisible(): boolean {
|
|
52
|
-
return scrollButtonState().visible;
|
|
53
|
-
}
|
|
54
|
-
export function getScrollButtonHovered(): boolean {
|
|
55
|
-
return scrollButtonState().hovered;
|
|
56
|
-
}
|
|
57
|
-
export function getScrollButtonWidget(): any {
|
|
58
|
-
return scrollButtonState().widget;
|
|
59
|
-
}
|
|
60
|
-
export function setScrollButtonVisible(visible: boolean): void {
|
|
61
|
-
scrollButtonState().visible = visible;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** 返回是否发生变化(调用方据此决定是否需要重渲染)。 */
|
|
65
|
-
export function setScrollButtonHovered(hovered: boolean): boolean {
|
|
66
|
-
if (hovered === scrollButtonState().hovered) return false;
|
|
67
|
-
scrollButtonState().hovered = hovered;
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function setScrollButtonWidget(widget: any): void {
|
|
72
|
-
scrollButtonState().widget = widget;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** teardown 全量清零(visible/hovered/widget/sync 调度)。 */
|
|
76
|
-
export function resetScrollButtonState(): void {
|
|
77
|
-
scrollButtonState().visible = false;
|
|
78
|
-
scrollButtonState().hovered = false;
|
|
79
|
-
scrollButtonState().widget = null;
|
|
80
|
-
scrollButtonSyncScheduled = false;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
let scrollButtonSyncScheduled = false;
|
|
84
|
-
|
|
85
26
|
// 交互开关只取决于配置模式:原实现按 isLazyProxyTui(toolMouseTui) 分两分支,
|
|
86
27
|
// 两分支恒真(0.84+ 惰性 Proxy 下判定不再影响开关),折叠为单条件。
|
|
87
28
|
export function toolMouseInteractionActive(): boolean {
|
|
@@ -92,125 +33,3 @@ export function toolMouseInteractionActive(): boolean {
|
|
|
92
33
|
export function fullscreenLazyTui(tui: any): boolean {
|
|
93
34
|
return isLazyProxyTui(tui) && tui.mode === "fullscreen";
|
|
94
35
|
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Reports whether fullscreen output is following the bottom of the context.
|
|
98
|
-
*/
|
|
99
|
-
export function isFullscreenAtBottom(tui: any): boolean {
|
|
100
|
-
const following =
|
|
101
|
-
tui.isFollowingOutput ??
|
|
102
|
-
tui.getPrimaryScrollView?.()?.isFollowingEnd ??
|
|
103
|
-
true;
|
|
104
|
-
return Boolean(following);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function formatShortcut(shortcut: string): string {
|
|
108
|
-
return shortcut
|
|
109
|
-
.split("+")
|
|
110
|
-
.map((part) =>
|
|
111
|
-
part.length <= 1
|
|
112
|
-
? part.toUpperCase()
|
|
113
|
-
: `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`,
|
|
114
|
-
)
|
|
115
|
-
.join("+");
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function isScrollBottomInput(data: string): boolean {
|
|
119
|
-
return matchesKey(data, SCROLL_BOTTOM_SHORTCUT);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function isScrollNavigationInput(data: string): boolean {
|
|
123
|
-
if (
|
|
124
|
-
matchesKey(data, "pageUp") ||
|
|
125
|
-
matchesKey(data, "pageDown") ||
|
|
126
|
-
ZENTUI_PAGE_UP_INPUT.test(data) ||
|
|
127
|
-
ZENTUI_PAGE_DOWN_INPUT.test(data) ||
|
|
128
|
-
// 官方 fullscreen viewport 的可滚动键(half-page/prompt/top/bottom)。
|
|
129
|
-
[
|
|
130
|
-
"tui.altScreen.pageUp",
|
|
131
|
-
"tui.altScreen.pageDown",
|
|
132
|
-
"tui.altScreen.halfPageUp",
|
|
133
|
-
"tui.altScreen.halfPageDown",
|
|
134
|
-
"tui.altScreen.previousPrompt",
|
|
135
|
-
"tui.altScreen.nextPrompt",
|
|
136
|
-
"tui.altScreen.top",
|
|
137
|
-
"tui.altScreen.bottom",
|
|
138
|
-
].some((key) => getKeybindings().matches(data, key as Keybinding))
|
|
139
|
-
) {
|
|
140
|
-
return true;
|
|
141
|
-
}
|
|
142
|
-
const packets = parseSgrMousePackets(data);
|
|
143
|
-
return Boolean(
|
|
144
|
-
packets?.some((packet) => {
|
|
145
|
-
const baseButton = packet.code & ~(4 | 8 | 16 | 32);
|
|
146
|
-
return packet.final === "M" && (baseButton === 64 || baseButton === 65);
|
|
147
|
-
}),
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Reports whether the context is at the bottom for the detected TUI mode.
|
|
153
|
-
*/
|
|
154
|
-
function isAtContextBottom(tui: any): boolean {
|
|
155
|
-
// 惰性 Proxy fullscreen:官方 viewport 以 isFollowingOutput 判定是否在底部。
|
|
156
|
-
if (fullscreenLazyTui(tui)) return isFullscreenAtBottom(tui);
|
|
157
|
-
return true;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export function hideScrollButton(tui: any): void {
|
|
161
|
-
const changed = getScrollButtonVisible() || getScrollButtonHovered();
|
|
162
|
-
setScrollButtonVisible(false);
|
|
163
|
-
setScrollButtonHovered(false);
|
|
164
|
-
if (changed) tui.requestRender?.();
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export function scheduleScrollButtonSync(tui: any, data: string): void {
|
|
168
|
-
if (
|
|
169
|
-
!fullscreenLazyTui(tui) ||
|
|
170
|
-
!toolMouseInteractionActive() ||
|
|
171
|
-
!isScrollNavigationInput(data) ||
|
|
172
|
-
scrollButtonSyncScheduled
|
|
173
|
-
)
|
|
174
|
-
return;
|
|
175
|
-
scrollButtonSyncScheduled = true;
|
|
176
|
-
const previousLines = tui.previousLines;
|
|
177
|
-
const check = (attempt: number) => {
|
|
178
|
-
scrollButtonSyncScheduled = false;
|
|
179
|
-
if (toolMouseTui !== tui) return;
|
|
180
|
-
// Pi renders on its own frame timer. Inspect the resulting viewport before
|
|
181
|
-
// showing the button so empty or non-scrollable contexts never flash it.
|
|
182
|
-
const rendered = tui.previousLines !== previousLines;
|
|
183
|
-
// fullscreen 下 isFollowingOutput 是即时状态,无需等待官方帧渲染。
|
|
184
|
-
if (!rendered && attempt < 4 && !fullscreenLazyTui(tui)) {
|
|
185
|
-
scrollButtonSyncScheduled = true;
|
|
186
|
-
const timer = setTimeout(() => check(attempt + 1), 16);
|
|
187
|
-
if (typeof timer === "object" && timer !== null && "unref" in timer) {
|
|
188
|
-
(timer as { unref: () => void }).unref();
|
|
189
|
-
}
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
const nextVisible = !isAtContextBottom(tui);
|
|
193
|
-
if (nextVisible !== getScrollButtonVisible()) {
|
|
194
|
-
setScrollButtonVisible(nextVisible);
|
|
195
|
-
tui.requestRender?.();
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
process.nextTick(() => check(0));
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export function updateScrollButtonFromInput(tui: any, data: string): void {
|
|
202
|
-
if (!fullscreenLazyTui(tui) || !toolMouseInteractionActive()) return;
|
|
203
|
-
if (matchesKey(data, "enter") || matchesKey(data, "return"))
|
|
204
|
-
hideScrollButton(tui);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export function renderScrollButton(width: number, theme: any): string[] {
|
|
208
|
-
if (!getScrollButtonVisible() || !fullscreenLazyTui(toolMouseTui)) return [];
|
|
209
|
-
const shortcut = formatShortcut(SCROLL_BOTTOM_SHORTCUT);
|
|
210
|
-
const label = theme.fg(
|
|
211
|
-
getScrollButtonHovered() ? "text" : "accent",
|
|
212
|
-
`[ ↓ Back to bottom · ${shortcut} ]`,
|
|
213
|
-
);
|
|
214
|
-
const leftPad = Math.max(0, Math.floor((width - visibleWidth(label)) / 2));
|
|
215
|
-
return [`${" ".repeat(leftPad)}${truncateToWidth(label, width, "…")}`];
|
|
216
|
-
}
|
|
@@ -119,7 +119,7 @@ export function renderEditDiffResult(
|
|
|
119
119
|
showHashlineAnchors,
|
|
120
120
|
);
|
|
121
121
|
const palette = resolveDiffPalette(theme);
|
|
122
|
-
// Rich diffs use
|
|
122
|
+
// Rich diffs use pi-one-ui's self shell. Keep the panel transparent so the
|
|
123
123
|
// separator cannot leak toolSuccessBg across the entire new column.
|
|
124
124
|
const containerBgAnsi = undefined;
|
|
125
125
|
const language = resolveLanguageFromPath(options.filePath);
|
|
@@ -46,7 +46,7 @@ export function renderRichToolResult(
|
|
|
46
46
|
theme: any,
|
|
47
47
|
context: any,
|
|
48
48
|
writeMetadata: WriteExecutionMetadataStore,
|
|
49
|
-
/** Plain snapshot or live getter — getter lets /
|
|
49
|
+
/** Plain snapshot or live getter — getter lets /oneui panel changes repaint existing diffs. */
|
|
50
50
|
displayConfig: DisplayConfigInput = DEFAULT_TOOL_DISPLAY_CONFIG,
|
|
51
51
|
): any | undefined {
|
|
52
52
|
if (options?.isPartial || options?.isError || context?.isError)
|
|
@@ -213,8 +213,8 @@ type SettledExpandedChildCache = {
|
|
|
213
213
|
|
|
214
214
|
function toolIoView(tool: any): ExpandedToolIoView | undefined {
|
|
215
215
|
for (const candidate of [
|
|
216
|
-
tool?.rendererState?.
|
|
217
|
-
tool?.state?.
|
|
216
|
+
tool?.rendererState?.oneuiIoView,
|
|
217
|
+
tool?.state?.oneuiIoView,
|
|
218
218
|
tool?.resultRendererComponent,
|
|
219
219
|
]) {
|
|
220
220
|
if (isExpandedToolIoView(candidate)) {
|
|
@@ -237,7 +237,7 @@ function ioRenderRevision(
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
export class ToolGroupComponent extends Container {
|
|
240
|
-
readonly toolCallId = `
|
|
240
|
+
readonly toolCallId = `oneui-tool-group-${nextGroupId++}`;
|
|
241
241
|
readonly toolName = "Tool group";
|
|
242
242
|
private _expanded = false;
|
|
243
243
|
/** 分组是否展开(只读;测试与外部读状态用)。 */
|