pi-one-ui 0.8.0 → 0.9.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.
- package/CHANGELOG.md +48 -1
- package/LICENSE +1 -1
- package/README.en.md +12 -10
- package/README.md +12 -10
- package/THIRD-PARTY-NOTICES.md +98 -0
- package/extensions/app/config/renderer.ts +1 -12
- package/extensions/app/config/shell.ts +5 -5
- package/extensions/app/runtime/tui-runtime.ts +6 -6
- package/extensions/app/settings/controller.ts +1 -1
- package/extensions/layouts/overlay/settings-panel.ts +9 -8
- package/extensions/layouts/overlay/settings-previews.ts +1 -1
- package/extensions/layouts/{context → transcript}/index.ts +8 -8
- package/extensions/layouts/{context → transcript}/renderer/compact-mode.ts +4 -4
- package/extensions/layouts/{context → transcript}/renderer/default-mode.ts +37 -7
- package/extensions/layouts/{context → transcript}/renderer/index.ts +8 -8
- package/extensions/layouts/{context → transcript}/renderer/mouse/hover.ts +0 -4
- package/extensions/layouts/{context → transcript}/renderer/mouse/interaction.ts +104 -93
- package/extensions/layouts/{context → transcript}/renderer/mouse/layout.ts +0 -4
- package/extensions/layouts/transcript/renderer/mouse/scroll.ts +35 -0
- package/extensions/layouts/{context → transcript}/renderer/tool/message-display.ts +1 -1
- package/extensions/layouts/{context → transcript}/renderer/tool/names.ts +19 -2
- package/extensions/layouts/{context/renderer/context-refresh.ts → transcript/renderer/transcript-refresh.ts} +6 -6
- package/extensions/layouts/{context → transcript}/thinking/compact-thinking.ts +4 -4
- package/extensions/tools/component-tree.ts +1 -1
- package/extensions/tools/patch-keys.ts +0 -3
- package/package.json +3 -2
- package/extensions/layouts/context/renderer/mouse/scroll.ts +0 -216
- /package/extensions/app/config/{context.ts → user-messages.ts} +0 -0
- /package/extensions/layouts/{context → transcript}/message/user-message-osc.ts +0 -0
- /package/extensions/layouts/{context → transcript}/message/user-message-styles.ts +0 -0
- /package/extensions/layouts/{context → transcript}/message/user-message.ts +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/markdown-enhance.ts +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/mouse/packets.ts +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/tool/diff/ATTRIBUTION.md +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/tool/diff/ansi-utils.ts +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/tool/diff/diff-component.ts +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/tool/diff/diff-edit-render.ts +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/tool/diff/diff-header.ts +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/tool/diff/diff-highlight.ts +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/tool/diff/diff-inline.ts +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/tool/diff/diff-layout.ts +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/tool/diff/diff-limits.ts +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/tool/diff/diff-palette.ts +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/tool/diff/diff-parse.ts +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/tool/diff/diff-presentation.ts +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/tool/diff/diff-renderer.ts +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/tool/diff/diff-text.ts +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/tool/diff/diff-write-render.ts +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/tool/diff/index.ts +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/tool/diff/line-width-safety.ts +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/tool/diff/shiki-highlight.ts +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/tool/diff/write-execution.ts +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/tool/grouping.ts +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/tool/result.ts +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/tool/show-more-hint.ts +0 -0
- /package/extensions/layouts/{context → transcript}/renderer/tool/toggle-render-cache.ts +0 -0
- /package/extensions/layouts/{context → transcript}/summary/core.ts +0 -0
- /package/extensions/layouts/{context → transcript}/summary/index.ts +0 -0
|
@@ -5,7 +5,12 @@
|
|
|
5
5
|
* 生命周期对齐 compact-mode:installDefaultMode → hooks.shutdown。
|
|
6
6
|
*/
|
|
7
7
|
import { ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
Text,
|
|
10
|
+
truncateToWidth,
|
|
11
|
+
visibleWidth,
|
|
12
|
+
wrapTextWithAnsi,
|
|
13
|
+
} from "@earendil-works/pi-tui";
|
|
9
14
|
import {
|
|
10
15
|
type CompactStyleMode,
|
|
11
16
|
config,
|
|
@@ -271,10 +276,18 @@ function createPolishedTool(
|
|
|
271
276
|
visualState === "success"
|
|
272
277
|
? `${BRIGHT_GREEN}${rawIcon}${ANSI_FG_RESET}`
|
|
273
278
|
: theme.fg(toolIconColor(context), rawIcon);
|
|
279
|
+
const summaryTitle =
|
|
280
|
+
label === toolName ? humanizeToolLabel(label) : label;
|
|
274
281
|
const summary = toolCallSummary(toolName, args, {
|
|
275
|
-
title:
|
|
282
|
+
title: summaryTitle,
|
|
276
283
|
variant: "default",
|
|
277
284
|
});
|
|
285
|
+
const expanded = Boolean(context?.expanded);
|
|
286
|
+
// 展开且仍在执行的卡展示完整命令;折叠卡与已结束的卡继续用截断摘要。
|
|
287
|
+
const fullCallText =
|
|
288
|
+
expanded && isPending
|
|
289
|
+
? `${toolCallSummary(toolName, args, { title: summaryTitle, variant: "default", fullInput: true }).main}${summary.detail}`
|
|
290
|
+
: "";
|
|
278
291
|
let writeStatsText = "";
|
|
279
292
|
let writeStatsStyled = "";
|
|
280
293
|
if (toolName === "write" && visualState === "success") {
|
|
@@ -293,23 +306,40 @@ function createPolishedTool(
|
|
|
293
306
|
const extraStyled = writeStatsStyled || theme.fg("dim", summary.detail);
|
|
294
307
|
let cachedWidth: number | undefined;
|
|
295
308
|
let cachedLine: string | undefined;
|
|
296
|
-
|
|
309
|
+
let cachedLines: string[] | undefined;
|
|
297
310
|
return {
|
|
298
311
|
render(width: number) {
|
|
299
|
-
if (
|
|
300
|
-
return
|
|
312
|
+
if (cachedLines !== undefined && cachedWidth === width)
|
|
313
|
+
return cachedLines;
|
|
301
314
|
const viewportWidth = toolViewportWidth(width);
|
|
302
315
|
// 展开态贴左(外层 Box 已 pad 1);折叠 self-shell 保留 1 格前导空格
|
|
303
316
|
const lead = expanded ? "" : " ";
|
|
317
|
+
cachedWidth = width;
|
|
318
|
+
if (fullCallText) {
|
|
319
|
+
// 完整文本按换行与视口宽度折行,续行对齐命令起始列
|
|
320
|
+
const prefix = `${lead}${icon} `;
|
|
321
|
+
const indent = " ".repeat(visibleWidth(prefix));
|
|
322
|
+
cachedLines = wrapTextWithAnsi(
|
|
323
|
+
fullCallText,
|
|
324
|
+
Math.max(1, viewportWidth - visibleWidth(prefix)),
|
|
325
|
+
).map((line, index) =>
|
|
326
|
+
truncateToWidth(
|
|
327
|
+
`${index === 0 ? prefix : indent}${theme.fg("toolTitle", line)}`,
|
|
328
|
+
viewportWidth,
|
|
329
|
+
"",
|
|
330
|
+
),
|
|
331
|
+
);
|
|
332
|
+
return cachedLines;
|
|
333
|
+
}
|
|
304
334
|
const callWidth = Math.max(
|
|
305
335
|
0,
|
|
306
336
|
viewportWidth - visibleWidth(icon) - 1 - (expanded ? 0 : 1),
|
|
307
337
|
);
|
|
308
338
|
const mainWidth = Math.max(0, callWidth - visibleWidth(extraText));
|
|
309
|
-
cachedWidth = width;
|
|
310
339
|
// 纯文本先截断再着色(省略号不带 ANSI);从头截断,与多 tool 一致
|
|
311
340
|
cachedLine = `${lead}${icon} ${theme.fg("toolTitle", headTruncateToWidth(summary.main, mainWidth))}${extraStyled}`;
|
|
312
|
-
|
|
341
|
+
cachedLines = [truncateToWidth(cachedLine, viewportWidth, "")];
|
|
342
|
+
return cachedLines;
|
|
313
343
|
},
|
|
314
344
|
invalidate() {},
|
|
315
345
|
};
|
|
@@ -62,7 +62,7 @@ import {
|
|
|
62
62
|
|
|
63
63
|
let compactModeHooks: CompactModeHooks | undefined;
|
|
64
64
|
|
|
65
|
-
function
|
|
65
|
+
function refreshCurrentTranscript(
|
|
66
66
|
ctx?: any,
|
|
67
67
|
toolGrouping?: ToolGroupingHooks,
|
|
68
68
|
render?: ToolMouseServices["render"],
|
|
@@ -104,15 +104,15 @@ function applyStyleMode(
|
|
|
104
104
|
tui.terminal?.write?.(TOOL_MOUSE_DISABLE);
|
|
105
105
|
}
|
|
106
106
|
} else if (mode === "compact") {
|
|
107
|
-
// Enter compact mode after collecting the current
|
|
107
|
+
// Enter compact mode after collecting the current transcript and syncing ownership.
|
|
108
108
|
syncCompactMode(ctx);
|
|
109
109
|
}
|
|
110
110
|
// Reshape immediately and once more after the panel/custom UI unmounts and
|
|
111
|
-
// the main
|
|
112
|
-
|
|
111
|
+
// the main transcript is mounted again.
|
|
112
|
+
refreshCurrentTranscript(ctx, toolGrouping, render);
|
|
113
113
|
scheduleSessionRender(() => {
|
|
114
114
|
if (mode === "compact") syncCompactMode(ctx);
|
|
115
|
-
|
|
115
|
+
refreshCurrentTranscript(ctx, toolGrouping, render);
|
|
116
116
|
});
|
|
117
117
|
ctx.ui.notify(`Claude Code style: ${mode}`, "info");
|
|
118
118
|
}
|
|
@@ -203,7 +203,7 @@ export default function (
|
|
|
203
203
|
// 配置(如 expandedPreviewMaxLines / excludeRenderers)变化可能改变
|
|
204
204
|
// 渲染结果:丢弃工具卡跨 toggle 缓存,下次 updateDisplay 重建。
|
|
205
205
|
installation?.toggleRenderCache.clear();
|
|
206
|
-
|
|
206
|
+
refreshCurrentTranscript(
|
|
207
207
|
ctx,
|
|
208
208
|
installation?.toolGrouping,
|
|
209
209
|
options.services?.render,
|
|
@@ -243,7 +243,7 @@ export default function (
|
|
|
243
243
|
hooks.toolGrouping.setTheme(ctx.ui.theme);
|
|
244
244
|
hooks.toggleRenderCache.setTheme(ctx.ui.theme);
|
|
245
245
|
setMessageDisplayTheme(ctx.ui.theme);
|
|
246
|
-
// Collect the resumed
|
|
246
|
+
// Collect the resumed transcript before syncing compact patches and expansion state.
|
|
247
247
|
syncCompactMode(ctx);
|
|
248
248
|
// compact-thinking 的 session_start 处理在本 handler 之后执行(在其之上再装
|
|
249
249
|
// 一层 updateContent);延迟再同步一次,保证 compact 补丁最终位于外层。
|
|
@@ -253,7 +253,7 @@ export default function (
|
|
|
253
253
|
|
|
254
254
|
pi.on("session_compact", async (event, ctx) => {
|
|
255
255
|
const hooks = ensureTuiInstallation(ctx);
|
|
256
|
-
// Compaction rebuilds the
|
|
256
|
+
// Compaction rebuilds the transcript without session_start. Rebind after
|
|
257
257
|
// other TUI extensions may have replaced the root input dispatcher.
|
|
258
258
|
if (ctx?.mode === "tui" && ctx?.hasUI)
|
|
259
259
|
installToolMouseInteraction(ctx, mouseOwner, options.services);
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
type ToolIoSection,
|
|
13
13
|
} from "../tool/result.ts";
|
|
14
14
|
import { type ComponentRowHit, componentAtLocalRow } from "./layout.ts";
|
|
15
|
-
import { setScrollButtonHovered } from "./scroll.ts";
|
|
16
15
|
|
|
17
16
|
type SharedToolHoverState = { toolCallId: string | null };
|
|
18
17
|
|
|
@@ -132,7 +131,6 @@ export function cachedFullscreenComponentAtRow(
|
|
|
132
131
|
|
|
133
132
|
/** fullscreen 鼠标悬停目标。 */
|
|
134
133
|
export type FullscreenHoverTarget =
|
|
135
|
-
| { kind: "button" }
|
|
136
134
|
| { kind: "group"; component: ToolGroupComponent }
|
|
137
135
|
| { kind: "thinking"; component: ThinkingPreviewBlock }
|
|
138
136
|
| { kind: "message"; component: any }
|
|
@@ -169,7 +167,5 @@ export function applyFullscreenHover(
|
|
|
169
167
|
const nextView = target?.kind === "tool" ? target.view : null;
|
|
170
168
|
const nextSection = target?.kind === "tool" ? target.section : null;
|
|
171
169
|
if (setHoveredToolIo(nextView, nextSection)) changed = true;
|
|
172
|
-
const nextButton = target?.kind === "button";
|
|
173
|
-
if (setScrollButtonHovered(nextButton)) changed = true;
|
|
174
170
|
if (changed) tui.requestRender?.();
|
|
175
171
|
}
|
|
@@ -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;
|
|
@@ -101,6 +90,11 @@ 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
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
|
|
|
@@ -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 ||
|
|
@@ -299,14 +281,11 @@ function releaseFullscreenToolMouseMotion(tui?: any): void {
|
|
|
299
281
|
}
|
|
300
282
|
|
|
301
283
|
const FULLSCREEN_VIEWPORT_PATCH = Symbol("pi-one-ui.fullscreen-viewport-patch");
|
|
302
|
-
const FULLSCREEN_WHEEL_SCROLL_ORIGINAL = Symbol(
|
|
303
|
-
"pi-one-ui.fullscreen-wheel-scroll-original",
|
|
304
|
-
);
|
|
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,26 +938,24 @@ 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
|
|
|
944
957
|
/**
|
|
945
|
-
* Installs the
|
|
958
|
+
* Installs the transcript mouse route through the shared input router.
|
|
946
959
|
*
|
|
947
960
|
* @param ctx Active TUI extension context.
|
|
948
961
|
* @param owner Ownership token used to make teardown safe across reloads.
|
|
@@ -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,
|
|
@@ -1026,7 +1037,7 @@ export function scheduleSessionRender(refresh?: () => void): void {
|
|
|
1026
1037
|
const tui = getToolMouseTui();
|
|
1027
1038
|
if (!tui || typeof tui.requestRender !== "function") return;
|
|
1028
1039
|
if (sessionRenderTimer) clearTimeout(sessionRenderTimer);
|
|
1029
|
-
// Restored
|
|
1040
|
+
// Restored transcript components are populated at different points for startup, reload,
|
|
1030
1041
|
// and session replacement. Repaint after session_start and the surrounding UI
|
|
1031
1042
|
// rebuild finish so messages are not left hidden until the next terminal input.
|
|
1032
1043
|
sessionRenderTimer = setTimeout(() => {
|
|
@@ -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) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { config } from "../../../../app/config/renderer.ts";
|
|
2
|
+
import { isLazyProxyTui } from "../../../../tools/fullscreen-detect.ts";
|
|
3
|
+
import {
|
|
4
|
+
patchRegistry,
|
|
5
|
+
TOOL_MOUSE_TUI_SLOT,
|
|
6
|
+
} from "../../../../tools/patch-keys.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 当前安装的 tui 宿主。由 mouse-interaction 经 setToolMouseTui 维护;
|
|
10
|
+
* 跨模块一律经绑定/setter 访问。
|
|
11
|
+
*
|
|
12
|
+
* 状态镜像到 globalThis(Symbol 槽):jiti 转译下经 re-export 链读取的
|
|
13
|
+
* 模块级 let 绑定是初始值快照(死绑定,实测恒 null),函数调用才是活引用。
|
|
14
|
+
* 跨模块读取一律用 getToolMouseTui(),避免拿到加载时的快照。
|
|
15
|
+
*/
|
|
16
|
+
patchRegistry.ensure(TOOL_MOUSE_TUI_SLOT, () => null);
|
|
17
|
+
export let toolMouseTui: any = null;
|
|
18
|
+
export function getToolMouseTui(): any {
|
|
19
|
+
return patchRegistry.get(TOOL_MOUSE_TUI_SLOT);
|
|
20
|
+
}
|
|
21
|
+
export function setToolMouseTui(tui: any): void {
|
|
22
|
+
toolMouseTui = tui;
|
|
23
|
+
patchRegistry.install(TOOL_MOUSE_TUI_SLOT, tui);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 交互开关只取决于配置模式:原实现按 isLazyProxyTui(toolMouseTui) 分两分支,
|
|
27
|
+
// 两分支恒真(0.84+ 惰性 Proxy 下判定不再影响开关),折叠为单条件。
|
|
28
|
+
export function toolMouseInteractionActive(): boolean {
|
|
29
|
+
return config.mode !== "off";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 惰性 Proxy 官方 fullscreen(TuiAltScreen)判定。 */
|
|
33
|
+
export function fullscreenLazyTui(tui: any): boolean {
|
|
34
|
+
return isLazyProxyTui(tui) && tui.mode === "fullscreen";
|
|
35
|
+
}
|
|
@@ -173,7 +173,7 @@ export function isMessageDisplayComponent(value: any): boolean {
|
|
|
173
173
|
}
|
|
174
174
|
|
|
175
175
|
/**
|
|
176
|
-
* Refreshes mounted
|
|
176
|
+
* Refreshes mounted transcript message components after a mode change.
|
|
177
177
|
*/
|
|
178
178
|
export function refreshMessageDisplays(root: any): void {
|
|
179
179
|
walkComponentTree(root, (value: any) => {
|
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import { config } from "../../../../app/config/renderer.ts";
|
|
2
2
|
import { oneLine } from "../../../../tools/format.ts";
|
|
3
|
+
import { sanitizeToolResultText } from "../../../../tools/tool-result-sanitize.ts";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
/** 展开输入与 `formatToolInputArgs` 共用同一个字符数上限。 */
|
|
6
|
+
const MAX_FULL_INPUT_CHARS = 8_000;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 输入摘要的文本处理。折叠摘要压成一行并按 toolInputNameLength 截断;
|
|
10
|
+
* fullInput 保留完整内容与换行,供展开卡展示完整命令使用。
|
|
11
|
+
*/
|
|
12
|
+
function clipSummaryInput(value: unknown, fullInput: boolean): string {
|
|
13
|
+
if (!fullInput) return oneLine(value, config.toolInputNameLength);
|
|
14
|
+
const raw = String(value ?? "");
|
|
15
|
+
const text = sanitizeToolResultText(raw, MAX_FULL_INPUT_CHARS)
|
|
16
|
+
.replace(/\t/g, " ")
|
|
17
|
+
.trim();
|
|
18
|
+
return raw.length > MAX_FULL_INPUT_CHARS ? `${text}…` : text;
|
|
6
19
|
}
|
|
7
20
|
|
|
8
21
|
/**
|
|
@@ -31,6 +44,8 @@ export type ToolCallSummaryOptions = {
|
|
|
31
44
|
title?: string;
|
|
32
45
|
/** 文案变体;缺省 "default"。 */
|
|
33
46
|
variant?: ToolCallSummaryVariant;
|
|
47
|
+
/** true 时返回未截断且保留换行的完整输入;缺省 false。 */
|
|
48
|
+
fullInput?: boolean;
|
|
34
49
|
};
|
|
35
50
|
|
|
36
51
|
/**
|
|
@@ -46,6 +61,8 @@ export function toolCallSummary(
|
|
|
46
61
|
): { main: string; detail: string } {
|
|
47
62
|
const title = opts.title ?? humanizeToolLabel(toolName);
|
|
48
63
|
const variant = opts.variant ?? "default";
|
|
64
|
+
const clip = (value: unknown) =>
|
|
65
|
+
clipSummaryInput(value, opts.fullInput === true);
|
|
49
66
|
if (!args || typeof args !== "object") return { main: title, detail: "" };
|
|
50
67
|
const name = toolName.toLowerCase();
|
|
51
68
|
const value = (fallback: string, ...keys: string[]) => {
|