pi-cc-extensions 0.8.50 → 0.8.52
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/README.en.md +9 -12
- package/README.md +9 -12
- package/extensions/config/config.ts +10 -10
- package/extensions/config/panel.ts +113 -26
- package/extensions/feature/agent-autocomplete.ts +3 -1
- package/extensions/feature/agent-summary.ts +8 -8
- package/extensions/feature/compact-thinking.ts +116 -55
- package/extensions/feature/context.ts +1 -1
- package/extensions/feature/markdown-enhance.ts +4 -1
- package/extensions/feature/pi-startup-header.ts +3 -8
- package/extensions/feature/session-reference-core.ts +3 -2
- package/extensions/feature/session-reference.ts +49 -21
- package/extensions/index.ts +12 -0
- package/extensions/renderer/compact-mode.ts +949 -0
- package/extensions/renderer/index.ts +73 -17
- package/extensions/renderer/message-display.ts +17 -3
- package/extensions/renderer/mouse-hover.ts +130 -0
- package/extensions/renderer/mouse-interaction.ts +74 -532
- package/extensions/renderer/mouse-layout.ts +160 -0
- package/extensions/renderer/mouse-packets.ts +143 -0
- package/extensions/renderer/mouse-scroll.ts +184 -0
- package/extensions/renderer/show-more-hint.ts +1 -3
- package/extensions/renderer/tool-diff/diff-renderer.ts +59 -2
- package/extensions/renderer/tool-grouping.ts +9 -2
- package/extensions/renderer/tool-result.ts +24 -8
- package/extensions/utils/agent-summary.ts +16 -2
- package/package.json +3 -2
- package/themes/cc-dark.json +3 -2
- package/themes/cc-light.json +3 -3
|
@@ -1,25 +1,63 @@
|
|
|
1
|
-
import { getKeybindings, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
1
|
import { hasActiveTextPreview, showTextPreview } from "../feature/context.ts";
|
|
3
2
|
import { ToolGroupComponent } from "./tool-grouping.ts";
|
|
3
|
+
import { isCompactAssistantComponent, setHoveredCompactAssistant } from "./compact-mode.ts";
|
|
4
4
|
import { config } from "../config/config.ts";
|
|
5
5
|
import { isLazyProxyTui } from "../utils/fullscreen-detect.ts";
|
|
6
6
|
import { setToolTuiFullscreen } from "./show-more-hint.ts";
|
|
7
7
|
import {
|
|
8
8
|
type ExpandedToolIoView,
|
|
9
9
|
getActiveIoViewFrame,
|
|
10
|
-
invalidateIoView,
|
|
11
10
|
isExpandedToolIoView,
|
|
12
11
|
type IoViewFrameState,
|
|
13
12
|
setActiveIoViewFrame,
|
|
14
13
|
type ToolIoSection,
|
|
15
14
|
} from "./tool-result.ts";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
import {
|
|
16
|
+
collectToolComponents,
|
|
17
|
+
extractToolFramePlacements,
|
|
18
|
+
isSgrLeftPress,
|
|
19
|
+
isToolExecutionComponent,
|
|
20
|
+
parseSgrMousePackets,
|
|
21
|
+
stripTerminalSequences,
|
|
22
|
+
stripTerminalSequencesPreservingLayout,
|
|
23
|
+
toolFrameMarker,
|
|
24
|
+
type FrameToolPlacement,
|
|
25
|
+
type SgrMousePacket,
|
|
26
|
+
} from "./mouse-packets.ts";
|
|
27
|
+
import {
|
|
28
|
+
collectFullscreenToolCards,
|
|
29
|
+
componentAtLocalRow,
|
|
30
|
+
collapsedHintHitbox,
|
|
31
|
+
fullscreenContentWidth,
|
|
32
|
+
fullscreenLeafAt,
|
|
33
|
+
isScrollbarColumnAt,
|
|
34
|
+
} from "./mouse-layout.ts";
|
|
35
|
+
import {
|
|
36
|
+
fullscreenLazyTui,
|
|
37
|
+
hideScrollButton,
|
|
38
|
+
isScrollBottomInput,
|
|
39
|
+
renderScrollButton,
|
|
40
|
+
resetScrollButtonState,
|
|
41
|
+
scheduleScrollButtonSync,
|
|
42
|
+
setScrollButtonHovered,
|
|
43
|
+
setScrollButtonVisible,
|
|
44
|
+
setScrollButtonWidget,
|
|
45
|
+
setToolMouseTui,
|
|
46
|
+
scrollButtonVisible,
|
|
47
|
+
scrollButtonWidget,
|
|
48
|
+
toolMouseInteractionActive,
|
|
49
|
+
toolMouseTui,
|
|
50
|
+
updateScrollButtonFromInput,
|
|
51
|
+
} from "./mouse-scroll.ts";
|
|
52
|
+
import {
|
|
53
|
+
applyFullscreenHover,
|
|
54
|
+
cachedFullscreenComponentAtRow,
|
|
55
|
+
sharedToolHoverState,
|
|
56
|
+
setHoveredToolCallId,
|
|
57
|
+
setHoveredToolGroup,
|
|
58
|
+
setHoveredToolIo,
|
|
59
|
+
type FullscreenHoverTarget,
|
|
60
|
+
} from "./mouse-hover.ts";
|
|
23
61
|
|
|
24
62
|
type FrameToolRender = {
|
|
25
63
|
component: any;
|
|
@@ -27,17 +65,6 @@ type FrameToolRender = {
|
|
|
27
65
|
contentBoxLines: number;
|
|
28
66
|
};
|
|
29
67
|
|
|
30
|
-
/** Final painted placement of one outermost tool/group row after parent layout. */
|
|
31
|
-
type FrameToolPlacement = {
|
|
32
|
-
component: any;
|
|
33
|
-
componentRow: number;
|
|
34
|
-
lineIndex: number;
|
|
35
|
-
/** Marker-stripped final line text as painted after parent layout. */
|
|
36
|
-
finalLine: string;
|
|
37
|
-
view?: ExpandedToolIoView;
|
|
38
|
-
section?: ToolIoSection;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
68
|
type InteractionRegion = {
|
|
42
69
|
kind: "collapsed-hint" | "expanded-card" | "show-more" | "scroll-bottom";
|
|
43
70
|
row: number;
|
|
@@ -50,23 +77,14 @@ type InteractionRegion = {
|
|
|
50
77
|
|
|
51
78
|
type InteractionFrame = { regions: InteractionRegion[] };
|
|
52
79
|
|
|
53
|
-
/** Zero-width APC row marker (like pi CURSOR_MARKER); stripped before terminal output. */
|
|
54
|
-
const TOOL_FRAME_MARKER_RE = /_cc:t(\d+):(\d+)/g;
|
|
55
|
-
const TOOL_VIEW_MARKER_RE = /_cc:v(\d+):([io])/g;
|
|
56
|
-
const toolFrameMarker = (id: number, row: number) => `_cc:t${id}:${row}`;
|
|
57
|
-
|
|
58
80
|
const TOOL_MOUSE_WIDGET_KEY = "ccstyle-tool-mouse";
|
|
59
81
|
const TOOL_MOUSE_MOTION_ENABLE = "\x1b[?1003h\x1b[?1006h";
|
|
60
82
|
const TOOL_MOUSE_MOTION_DISABLE = "\x1b[?1003l";
|
|
61
83
|
const FULLSCREEN_MOTION_ENABLED = Symbol("ccstyle.fullscreen-motion-enabled");
|
|
62
|
-
const TOOL_HOVER_STATE_KEY = Symbol.for("pi.ccstyle.tool-hover-state");
|
|
63
84
|
const TOOL_MOUSE_OWNER_KEY = Symbol.for("pi.ccstyle.tool-mouse-owner");
|
|
64
85
|
const DEFAULT_TOOL_MOUSE_OWNER = {};
|
|
65
86
|
export const TOOL_MOUSE_DISABLE = "\x1b[?1006l\x1b[?1003l\x1b[?1000l";
|
|
66
|
-
|
|
67
|
-
const ZENTUI_PAGE_DOWN_INPUT = /^\x1b\[6;9(?::[12])?~$|^\x1b\[57422;9(?::[12])?u$|^\x1b\[1;6B$/;
|
|
68
|
-
const SCROLL_BOTTOM_SHORTCUT = "ctrl+end";
|
|
69
|
-
export let toolMouseTui: any = null;
|
|
87
|
+
|
|
70
88
|
let toolMouseUi: any = null;
|
|
71
89
|
let toolMouseInputUnsubscribe: (() => void) | null = null;
|
|
72
90
|
let toolMouseRenderPatchTui: any = null;
|
|
@@ -77,268 +95,12 @@ let toolMouseRawWrite: ((data: string) => unknown) | null = null;
|
|
|
77
95
|
let toolMouseInstallationOwner: object | null = null;
|
|
78
96
|
let fullscreenMotionTerminal: any = null;
|
|
79
97
|
let ownsFullscreenMotion = false;
|
|
80
|
-
let scrollButtonVisible = false;
|
|
81
|
-
let scrollButtonHovered = false;
|
|
82
|
-
let scrollButtonWidget: any = null;
|
|
83
|
-
let scrollButtonSyncScheduled = false;
|
|
84
98
|
let sessionRenderTimer: ReturnType<typeof setTimeout> | null = null;
|
|
85
|
-
type SharedToolHoverState = { toolCallId: string | null };
|
|
86
|
-
function sharedToolHoverState(): SharedToolHoverState {
|
|
87
|
-
const host = globalThis as any;
|
|
88
|
-
return (host[TOOL_HOVER_STATE_KEY] ??= { toolCallId: null });
|
|
89
|
-
}
|
|
90
|
-
export let hoveredToolCallId: string | null = sharedToolHoverState().toolCallId;
|
|
91
|
-
function setHoveredToolCallId(toolCallId: string | null): void {
|
|
92
|
-
hoveredToolCallId = toolCallId;
|
|
93
|
-
sharedToolHoverState().toolCallId = toolCallId;
|
|
94
|
-
}
|
|
95
|
-
export function isToolCallHovered(toolCallId: string | null | undefined): boolean {
|
|
96
|
-
return Boolean(toolCallId && sharedToolHoverState().toolCallId === toolCallId);
|
|
97
|
-
}
|
|
98
|
-
let hoveredToolGroup: ToolGroupComponent | null = null;
|
|
99
99
|
let latestInteractionFrame: InteractionFrame = { regions: [] };
|
|
100
100
|
|
|
101
|
-
function parseSgrMousePackets(data: string): SgrMousePacket[] | null {
|
|
102
|
-
const pattern = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
|
|
103
|
-
const packets: SgrMousePacket[] = [];
|
|
104
|
-
let offset = 0;
|
|
105
|
-
|
|
106
|
-
for (const match of data.matchAll(pattern)) {
|
|
107
|
-
if (match.index !== offset) return null;
|
|
108
|
-
offset = match.index + match[0].length;
|
|
109
|
-
packets.push({
|
|
110
|
-
code: Number(match[1]),
|
|
111
|
-
col: Number(match[2]),
|
|
112
|
-
row: Number(match[3]),
|
|
113
|
-
final: match[4] as "M" | "m",
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return packets.length > 0 && offset === data.length ? packets : null;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function isSgrLeftPress(packet: SgrMousePacket): boolean {
|
|
121
|
-
const baseButton = packet.code & ~(4 | 8 | 16 | 32);
|
|
122
|
-
return packet.final === "M" && baseButton === 0 && (packet.code & 32) === 0;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function stripTerminalSequencesPreservingLayout(value: string): string {
|
|
126
|
-
return value
|
|
127
|
-
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
128
|
-
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function stripTerminalSequences(value: string): string {
|
|
132
|
-
return stripTerminalSequencesPreservingLayout(value).replace(/\s+/g, " ").trim();
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function isToolExecutionComponent(value: any): boolean {
|
|
136
|
-
return Boolean(
|
|
137
|
-
value &&
|
|
138
|
-
typeof value === "object" &&
|
|
139
|
-
typeof value.toolCallId === "string" &&
|
|
140
|
-
typeof value.setExpanded === "function" &&
|
|
141
|
-
typeof value.render === "function",
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function collectToolComponents(component: any, tools: any[], seen = new Set<any>()): void {
|
|
146
|
-
if (!component || typeof component !== "object" || seen.has(component)) return;
|
|
147
|
-
seen.add(component);
|
|
148
|
-
if (isToolExecutionComponent(component)) {
|
|
149
|
-
tools.push(component);
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
if (Array.isArray(component.children)) {
|
|
153
|
-
for (const child of component.children) collectToolComponents(child, tools, seen);
|
|
154
|
-
}
|
|
155
|
-
try {
|
|
156
|
-
const mounted = component.getMountedRoots?.();
|
|
157
|
-
if (Array.isArray(mounted)) {
|
|
158
|
-
for (const root of mounted) collectToolComponents(root, tools, seen);
|
|
159
|
-
}
|
|
160
|
-
} catch {
|
|
161
|
-
// renderer 切换中的惰性 Proxy 可能暂时没有 mounted roots。
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function stripToolFrameMarkers(line: string): string {
|
|
166
|
-
return line.replace(TOOL_FRAME_MARKER_RE, "").replace(TOOL_VIEW_MARKER_RE, "");
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function extractToolFramePlacements(
|
|
170
|
-
lines: string[],
|
|
171
|
-
idToComponent: Map<number, any>,
|
|
172
|
-
idToView: Map<number, ExpandedToolIoView>,
|
|
173
|
-
): { lines: string[]; placements: FrameToolPlacement[] } {
|
|
174
|
-
const placements: FrameToolPlacement[] = [];
|
|
175
|
-
const cleaned = lines.map((line, lineIndex) => {
|
|
176
|
-
const toolMatches = [...line.matchAll(TOOL_FRAME_MARKER_RE)];
|
|
177
|
-
const viewMatches = [...line.matchAll(TOOL_VIEW_MARKER_RE)];
|
|
178
|
-
const finalLine = stripToolFrameMarkers(line);
|
|
179
|
-
let view: ExpandedToolIoView | undefined;
|
|
180
|
-
let section: ToolIoSection | undefined;
|
|
181
|
-
for (const match of viewMatches) {
|
|
182
|
-
const candidate = idToView.get(Number(match[1]));
|
|
183
|
-
if (!candidate) continue;
|
|
184
|
-
view = candidate;
|
|
185
|
-
section = match[2] === "i" ? "input" : "output";
|
|
186
|
-
break;
|
|
187
|
-
}
|
|
188
|
-
for (const match of toolMatches) {
|
|
189
|
-
const component = idToComponent.get(Number(match[1]));
|
|
190
|
-
if (!component) continue;
|
|
191
|
-
placements.push({
|
|
192
|
-
component,
|
|
193
|
-
componentRow: Number(match[2]),
|
|
194
|
-
lineIndex,
|
|
195
|
-
finalLine,
|
|
196
|
-
view,
|
|
197
|
-
section,
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
return finalLine;
|
|
201
|
-
});
|
|
202
|
-
return { lines: cleaned, placements };
|
|
203
|
-
}
|
|
204
|
-
|
|
205
101
|
/** Summary markers used by Pi and ccstyle; unlike the trailing hint, these survive truncation. */
|
|
206
102
|
const COLLAPSED_TOOL_SUMMARY = /^\s*(?:↳|└|⎿|●|✓|✗|…)/;
|
|
207
103
|
|
|
208
|
-
function toolMouseInteractionActive(): boolean {
|
|
209
|
-
if (config.mode === "off") return false;
|
|
210
|
-
if (isLazyProxyTui(toolMouseTui)) return true;
|
|
211
|
-
return true;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function formatShortcut(shortcut: string): string {
|
|
215
|
-
return shortcut
|
|
216
|
-
.split("+")
|
|
217
|
-
.map((part) =>
|
|
218
|
-
part.length <= 1 ? part.toUpperCase() : `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`,
|
|
219
|
-
)
|
|
220
|
-
.join("+");
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function isScrollBottomInput(data: string): boolean {
|
|
224
|
-
return matchesKey(data, SCROLL_BOTTOM_SHORTCUT);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function wheelDirection(data: string): "up" | "down" | null {
|
|
228
|
-
const packets = parseSgrMousePackets(data);
|
|
229
|
-
for (const packet of packets ?? []) {
|
|
230
|
-
if (packet.final !== "M") continue;
|
|
231
|
-
const baseButton = packet.code & ~(4 | 8 | 16 | 32);
|
|
232
|
-
if (baseButton === 64) return "up";
|
|
233
|
-
if (baseButton === 65) return "down";
|
|
234
|
-
}
|
|
235
|
-
return null;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function isScrollNavigationInput(data: string): boolean {
|
|
239
|
-
if (
|
|
240
|
-
matchesKey(data, "pageUp") ||
|
|
241
|
-
matchesKey(data, "pageDown") ||
|
|
242
|
-
ZENTUI_PAGE_UP_INPUT.test(data) ||
|
|
243
|
-
ZENTUI_PAGE_DOWN_INPUT.test(data) ||
|
|
244
|
-
// 官方 fullscreen viewport 的可滚动键(half-page/prompt/top/bottom)。
|
|
245
|
-
[
|
|
246
|
-
"tui.altScreen.pageUp",
|
|
247
|
-
"tui.altScreen.pageDown",
|
|
248
|
-
"tui.altScreen.halfPageUp",
|
|
249
|
-
"tui.altScreen.halfPageDown",
|
|
250
|
-
"tui.altScreen.previousPrompt",
|
|
251
|
-
"tui.altScreen.nextPrompt",
|
|
252
|
-
"tui.altScreen.top",
|
|
253
|
-
"tui.altScreen.bottom",
|
|
254
|
-
].some((key) => getKeybindings().matches(data, key))
|
|
255
|
-
) {
|
|
256
|
-
return true;
|
|
257
|
-
}
|
|
258
|
-
const packets = parseSgrMousePackets(data);
|
|
259
|
-
return Boolean(
|
|
260
|
-
packets?.some((packet) => {
|
|
261
|
-
const baseButton = packet.code & ~(4 | 8 | 16 | 32);
|
|
262
|
-
return packet.final === "M" && (baseButton === 64 || baseButton === 65);
|
|
263
|
-
}),
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function isAtTranscriptBottom(tui: any): boolean {
|
|
268
|
-
// 惰性 Proxy fullscreen:官方 viewport 以 isFollowingOutput 判定是否在底部。
|
|
269
|
-
if (fullscreenLazyTui(tui)) return isFullscreenAtBottom(tui);
|
|
270
|
-
return true;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function hideScrollButton(tui: any): void {
|
|
274
|
-
const changed = scrollButtonVisible || scrollButtonHovered;
|
|
275
|
-
scrollButtonVisible = false;
|
|
276
|
-
scrollButtonHovered = false;
|
|
277
|
-
if (changed) tui.requestRender?.();
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function scheduleScrollButtonSync(tui: any, data: string): void {
|
|
281
|
-
if (
|
|
282
|
-
!fullscreenLazyTui(tui) ||
|
|
283
|
-
!toolMouseInteractionActive() ||
|
|
284
|
-
!isScrollNavigationInput(data) ||
|
|
285
|
-
scrollButtonSyncScheduled
|
|
286
|
-
)
|
|
287
|
-
return;
|
|
288
|
-
scrollButtonSyncScheduled = true;
|
|
289
|
-
const previousLines = tui.previousLines;
|
|
290
|
-
const check = (attempt: number) => {
|
|
291
|
-
scrollButtonSyncScheduled = false;
|
|
292
|
-
if (toolMouseTui !== tui) return;
|
|
293
|
-
// Pi renders on its own frame timer. Inspect the resulting viewport before
|
|
294
|
-
// showing the button so empty or non-scrollable transcripts never flash it.
|
|
295
|
-
const rendered = tui.previousLines !== previousLines;
|
|
296
|
-
// fullscreen 下 isFollowingOutput 是即时状态,无需等待官方帧渲染。
|
|
297
|
-
if (!rendered && attempt < 4 && !fullscreenLazyTui(tui)) {
|
|
298
|
-
scrollButtonSyncScheduled = true;
|
|
299
|
-
const timer = setTimeout(() => check(attempt + 1), 16);
|
|
300
|
-
if (typeof timer === "object" && timer !== null && "unref" in timer) {
|
|
301
|
-
(timer as { unref: () => void }).unref();
|
|
302
|
-
}
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
const nextVisible = !isAtTranscriptBottom(tui);
|
|
306
|
-
if (nextVisible !== scrollButtonVisible) {
|
|
307
|
-
scrollButtonVisible = nextVisible;
|
|
308
|
-
tui.requestRender?.();
|
|
309
|
-
}
|
|
310
|
-
};
|
|
311
|
-
process.nextTick(() => check(0));
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function updateScrollButtonFromInput(tui: any, data: string): void {
|
|
315
|
-
if (!fullscreenLazyTui(tui) || !toolMouseInteractionActive()) return;
|
|
316
|
-
if (matchesKey(data, "enter") || matchesKey(data, "return")) hideScrollButton(tui);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function renderComponentTree(component: any, width: number): string[] {
|
|
320
|
-
if (!component || typeof component !== "object") return [];
|
|
321
|
-
try {
|
|
322
|
-
const lines = component.render?.(width);
|
|
323
|
-
if (Array.isArray(lines) && lines.length > 0) return lines;
|
|
324
|
-
} catch {
|
|
325
|
-
// Fall through to children for hidden container renderers.
|
|
326
|
-
}
|
|
327
|
-
if (!Array.isArray(component.children)) return [];
|
|
328
|
-
return component.children.flatMap((child: any) => renderComponentTree(child, width));
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
let hoveredToolIoView: ExpandedToolIoView | null = null;
|
|
332
|
-
let hoveredToolIoSection: ToolIoSection | null = null;
|
|
333
|
-
|
|
334
|
-
function collapsedHintHitbox(line: string): { startCol: number; endCol: number } | null {
|
|
335
|
-
const plain = stripTerminalSequencesPreservingLayout(line);
|
|
336
|
-
const match = /(\([^()\n]* \/ click\)|click to show more|to show more)(?=\)?\s*$)/.exec(plain);
|
|
337
|
-
if (!match?.[1]) return null;
|
|
338
|
-
const startCol = visibleWidth(plain.slice(0, match.index)) + 1;
|
|
339
|
-
return { startCol, endCol: startCol + visibleWidth(match[1]) - 1 };
|
|
340
|
-
}
|
|
341
|
-
|
|
342
104
|
function interactionRegionAt(packet: SgrMousePacket): InteractionRegion | null {
|
|
343
105
|
const matches = latestInteractionFrame.regions.filter(
|
|
344
106
|
(region) =>
|
|
@@ -368,41 +130,11 @@ function tryOpenToolIoShowMore(region: InteractionRegion): boolean {
|
|
|
368
130
|
return true;
|
|
369
131
|
}
|
|
370
132
|
|
|
371
|
-
export function setHoveredToolIo(
|
|
372
|
-
view: ExpandedToolIoView | null,
|
|
373
|
-
section: ToolIoSection | null,
|
|
374
|
-
): boolean {
|
|
375
|
-
// resultRendererComponent 可能是 Text/第三方 renderer;reload 后也可能残留旧实例。
|
|
376
|
-
const nextView = isExpandedToolIoView(view) ? view : null;
|
|
377
|
-
const nextSection = nextView ? section : null;
|
|
378
|
-
if (nextView === hoveredToolIoView && nextSection === hoveredToolIoSection) return false;
|
|
379
|
-
if (isExpandedToolIoView(hoveredToolIoView)) {
|
|
380
|
-
hoveredToolIoView.setHoveredSection(null);
|
|
381
|
-
invalidateIoView(hoveredToolIoView);
|
|
382
|
-
}
|
|
383
|
-
hoveredToolIoView = nextView;
|
|
384
|
-
hoveredToolIoSection = nextSection;
|
|
385
|
-
if (nextView) {
|
|
386
|
-
nextView.setHoveredSection(nextSection);
|
|
387
|
-
invalidateIoView(nextView);
|
|
388
|
-
}
|
|
389
|
-
return true;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
export function setHoveredToolGroup(group: ToolGroupComponent | null): boolean {
|
|
393
|
-
if (group === hoveredToolGroup) return false;
|
|
394
|
-
hoveredToolGroup?.setHintHovered(false);
|
|
395
|
-
hoveredToolGroup = group;
|
|
396
|
-
group?.setHintHovered(true);
|
|
397
|
-
return true;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
133
|
function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
|
|
401
134
|
if ((packet.code & 32) === 0 || packet.final !== "M") return;
|
|
402
135
|
const region = interactionRegionAt(packet);
|
|
403
136
|
const nextScrollButtonHovered = region?.kind === "scroll-bottom";
|
|
404
|
-
const scrollButtonChanged = nextScrollButtonHovered
|
|
405
|
-
scrollButtonHovered = nextScrollButtonHovered;
|
|
137
|
+
const scrollButtonChanged = setScrollButtonHovered(nextScrollButtonHovered);
|
|
406
138
|
const component = region?.component;
|
|
407
139
|
const nextToolCallId = region?.kind === "collapsed-hint" ? (component?.toolCallId ?? null) : null;
|
|
408
140
|
const nextGroup = component instanceof ToolGroupComponent ? component : null;
|
|
@@ -445,22 +177,6 @@ function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
445
177
|
return true;
|
|
446
178
|
}
|
|
447
179
|
|
|
448
|
-
function renderScrollButton(width: number, theme: any): string[] {
|
|
449
|
-
if (!scrollButtonVisible || !fullscreenLazyTui(toolMouseTui)) return [];
|
|
450
|
-
const shortcut = formatShortcut(SCROLL_BOTTOM_SHORTCUT);
|
|
451
|
-
const label = theme.fg(
|
|
452
|
-
scrollButtonHovered ? "text" : "accent",
|
|
453
|
-
`[ ↓ Back to bottom · ${shortcut} ]`,
|
|
454
|
-
);
|
|
455
|
-
const leftPad = Math.max(0, Math.floor((width - visibleWidth(label)) / 2));
|
|
456
|
-
return [`${" ".repeat(leftPad)}${truncateToWidth(label, width, "…")}`];
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/** 惰性 Proxy 官方 fullscreen(TuiAltScreen)判定。 */
|
|
460
|
-
function fullscreenLazyTui(tui: any): boolean {
|
|
461
|
-
return isLazyProxyTui(tui) && tui.mode === "fullscreen";
|
|
462
|
-
}
|
|
463
|
-
|
|
464
180
|
function officialFullscreenHasAllMotion(): boolean {
|
|
465
181
|
const term = process.env.TERM?.toLowerCase() ?? "";
|
|
466
182
|
return !(
|
|
@@ -521,133 +237,9 @@ function releaseFullscreenToolMouseMotion(tui?: any): void {
|
|
|
521
237
|
}
|
|
522
238
|
}
|
|
523
239
|
|
|
524
|
-
/** 官方 fullscreen:是否已跟随 transcript 底部(按钮隐藏条件)。 */
|
|
525
|
-
function isFullscreenAtBottom(tui: any): boolean {
|
|
526
|
-
const following = tui.isFollowingOutput ?? tui.getPrimaryScrollView?.()?.isFollowingEnd ?? true;
|
|
527
|
-
return Boolean(following);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
240
|
const FULLSCREEN_VIEWPORT_PATCH = Symbol("ccstyle.fullscreen-viewport-patch");
|
|
531
241
|
const FULLSCREEN_WHEEL_SCROLL_ORIGINAL = Symbol("ccstyle.fullscreen-wheel-scroll-original");
|
|
532
242
|
|
|
533
|
-
/** 布局树点查询:返回 (x,y) 处最深含行 leaf box(屏幕行 → 组件局部行)。 */
|
|
534
|
-
function fullscreenLeafAt(
|
|
535
|
-
layout: any,
|
|
536
|
-
x: number,
|
|
537
|
-
y: number,
|
|
538
|
-
): { box: any; localRow: number } | null {
|
|
539
|
-
const root = layout?.root;
|
|
540
|
-
if (!root) return null;
|
|
541
|
-
let best: { box: any; localRow: number } | null = null;
|
|
542
|
-
let bestDepth = -1;
|
|
543
|
-
const visit = (box: any, depth: number) => {
|
|
544
|
-
if (!box) return;
|
|
545
|
-
const clip = box.clip;
|
|
546
|
-
if (!clip || x < clip.x || x >= clip.x + clip.width || y < clip.y || y >= clip.y + clip.height)
|
|
547
|
-
return;
|
|
548
|
-
const isLeaf = !Array.isArray(box.children) || box.children.length === 0;
|
|
549
|
-
if (
|
|
550
|
-
isLeaf &&
|
|
551
|
-
y >= box.rect.y &&
|
|
552
|
-
y < box.rect.y + Math.max(1, box.rect.height) &&
|
|
553
|
-
depth > bestDepth
|
|
554
|
-
) {
|
|
555
|
-
best = { box, localRow: Math.max(0, y - box.rect.y) };
|
|
556
|
-
bestDepth = depth;
|
|
557
|
-
}
|
|
558
|
-
for (const child of box.children ?? []) visit(child, depth + 1);
|
|
559
|
-
};
|
|
560
|
-
visit(root, 0);
|
|
561
|
-
return best;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
/** leaf 自身不带 scrollView;内容宽度由最近的 scroll 祖先决定。 */
|
|
565
|
-
function fullscreenContentWidth(box: any, terminalWidth: number): number {
|
|
566
|
-
for (let current = box; current; current = current.parent) {
|
|
567
|
-
if (typeof current.scrollView?.getContentWidth === "function") {
|
|
568
|
-
return Math.max(1, current.scrollView.getContentWidth(terminalWidth));
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
return terminalWidth;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
/** 点击列是否为官方滚动条列(放行官方拖动)。 */
|
|
575
|
-
function isScrollbarColumnAt(layout: any, x: number): boolean {
|
|
576
|
-
let hit = false;
|
|
577
|
-
const visit = (box: any) => {
|
|
578
|
-
if (hit || !box) return;
|
|
579
|
-
if (box.scrollView?.isScrollbarVisible && x === box.rect.x + box.rect.width - 1) {
|
|
580
|
-
hit = true;
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
for (const child of box.children ?? []) visit(child);
|
|
584
|
-
};
|
|
585
|
-
visit(layout?.root);
|
|
586
|
-
return hit;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
type ComponentRowHit = {
|
|
590
|
-
component: any;
|
|
591
|
-
row: number;
|
|
592
|
-
/** 内部工具命中时所属的展开 group;普通卡点击仍折叠整个 group。 */
|
|
593
|
-
group?: ToolGroupComponent;
|
|
594
|
-
};
|
|
595
|
-
|
|
596
|
-
/**
|
|
597
|
-
* 布局 leaf box 的组件通常是容器(documentContainer/dock 容器等),工具卡与
|
|
598
|
-
* widget 在其 children 内。按局部行遍历组件树,定位实际命中的子组件。
|
|
599
|
-
*/
|
|
600
|
-
function componentAtLocalRow(
|
|
601
|
-
component: any,
|
|
602
|
-
localRow: number,
|
|
603
|
-
width: number,
|
|
604
|
-
): ComponentRowHit | null {
|
|
605
|
-
if (component instanceof ToolGroupComponent) {
|
|
606
|
-
// 展开的 group:头两行(空行 + 头行)归 group,其余行映射到内部工具。
|
|
607
|
-
const child = component.childAtRow(localRow, width);
|
|
608
|
-
return child ? { ...child, group: component } : { component, row: localRow };
|
|
609
|
-
}
|
|
610
|
-
if (isToolExecutionComponent(component)) {
|
|
611
|
-
return { component, row: localRow };
|
|
612
|
-
}
|
|
613
|
-
if (component === scrollButtonWidget) {
|
|
614
|
-
return { component, row: localRow };
|
|
615
|
-
}
|
|
616
|
-
if (!Array.isArray(component.children)) return null;
|
|
617
|
-
let offset = 0;
|
|
618
|
-
for (const child of component.children) {
|
|
619
|
-
let lines: string[] = [];
|
|
620
|
-
try {
|
|
621
|
-
const rendered = child.render?.(width);
|
|
622
|
-
if (Array.isArray(rendered)) lines = rendered.map((line) => String(line));
|
|
623
|
-
} catch {
|
|
624
|
-
lines = [];
|
|
625
|
-
}
|
|
626
|
-
if (localRow < offset + lines.length) {
|
|
627
|
-
return (
|
|
628
|
-
componentAtLocalRow(child, localRow - offset, width) ?? {
|
|
629
|
-
component: child,
|
|
630
|
-
row: localRow - offset,
|
|
631
|
-
}
|
|
632
|
-
);
|
|
633
|
-
}
|
|
634
|
-
offset += lines.length;
|
|
635
|
-
}
|
|
636
|
-
return null;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
/** fullscreen single-expand:group 作为整体,不继续递归其内部工具。 */
|
|
640
|
-
function collectFullscreenToolCards(component: any, out: any[], seen = new Set<any>()): void {
|
|
641
|
-
if (!component || typeof component !== "object" || seen.has(component)) return;
|
|
642
|
-
seen.add(component);
|
|
643
|
-
if (isToolExecutionComponent(component) || component instanceof ToolGroupComponent) {
|
|
644
|
-
out.push(component);
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
if (!Array.isArray(component.children)) return;
|
|
648
|
-
for (const child of component.children) collectFullscreenToolCards(child, out, seen);
|
|
649
|
-
}
|
|
650
|
-
|
|
651
243
|
/**
|
|
652
244
|
* 官方 fullscreen 工具卡点击:collapsed hint 点击展开
|
|
653
245
|
* (有且仅保持一个展开:展开前收起其他工具卡),expanded 整卡二次点击收起,
|
|
@@ -681,7 +273,8 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
681
273
|
if (typeof line !== "string" || /\x1b]8;[^;]*;/.test(line)) return false;
|
|
682
274
|
const isTool = isToolExecutionComponent(component);
|
|
683
275
|
const isGroup = component instanceof ToolGroupComponent;
|
|
684
|
-
|
|
276
|
+
const isAssistant = isCompactAssistantComponent(component);
|
|
277
|
+
if (!isTool && !isGroup && !isAssistant) return false;
|
|
685
278
|
if (!component.expanded) {
|
|
686
279
|
// collapsed 仅按钮文本可展开,不能把同一行正文/留白变成点击区。
|
|
687
280
|
const hint = collapsedHintHitbox(line);
|
|
@@ -724,51 +317,14 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
724
317
|
setHoveredToolCallId(null);
|
|
725
318
|
setHoveredToolGroup(null);
|
|
726
319
|
setHoveredToolIo(null, null);
|
|
320
|
+
setHoveredCompactAssistant(null);
|
|
727
321
|
card.invalidate?.();
|
|
728
322
|
tui.requestRender?.();
|
|
729
323
|
return true;
|
|
730
324
|
}
|
|
731
325
|
|
|
732
|
-
/** hover 与点击共用组件定位;同一布局下按容器/宽度/行缓存,避免 motion 重复渲染。 */
|
|
733
|
-
let fullscreenHoverCacheLayout: unknown = null;
|
|
734
|
-
let fullscreenHoverComponentCache = new WeakMap<object, Map<string, ComponentRowHit | null>>();
|
|
735
|
-
|
|
736
|
-
function cachedFullscreenComponentAtRow(
|
|
737
|
-
layout: any,
|
|
738
|
-
container: any,
|
|
739
|
-
row: number,
|
|
740
|
-
width: number,
|
|
741
|
-
): ComponentRowHit | null {
|
|
742
|
-
if (!container || typeof container !== "object") return null;
|
|
743
|
-
if (fullscreenHoverCacheLayout !== layout) {
|
|
744
|
-
fullscreenHoverCacheLayout = layout;
|
|
745
|
-
fullscreenHoverComponentCache = new WeakMap();
|
|
746
|
-
}
|
|
747
|
-
let rows = fullscreenHoverComponentCache.get(container);
|
|
748
|
-
if (!rows) {
|
|
749
|
-
rows = new Map();
|
|
750
|
-
fullscreenHoverComponentCache.set(container, rows);
|
|
751
|
-
}
|
|
752
|
-
const key = `${width}:${row}`;
|
|
753
|
-
if (rows.has(key)) return rows.get(key) ?? null;
|
|
754
|
-
const hit = componentAtLocalRow(container, row, width);
|
|
755
|
-
rows.set(key, hit);
|
|
756
|
-
return hit;
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
/** fullscreen 鼠标悬停目标。 */
|
|
760
|
-
type FullscreenHoverTarget =
|
|
761
|
-
| { kind: "button" }
|
|
762
|
-
| { kind: "group"; component: ToolGroupComponent }
|
|
763
|
-
| {
|
|
764
|
-
kind: "tool";
|
|
765
|
-
component: any;
|
|
766
|
-
view: ExpandedToolIoView | null;
|
|
767
|
-
section: ToolIoSection | null;
|
|
768
|
-
};
|
|
769
|
-
|
|
770
326
|
/**
|
|
771
|
-
* fullscreen
|
|
327
|
+
* fullscreen 鼠标悬停:collapsed 卡 [click to show more] hint、
|
|
772
328
|
* expanded 卡截断头 show-more、回到底部按钮。motion 不 consume,官方链照常。
|
|
773
329
|
*/
|
|
774
330
|
function handleFullscreenToolHover(tui: any, packet: SgrMousePacket): void {
|
|
@@ -825,36 +381,15 @@ function handleFullscreenToolHover(tui: any, packet: SgrMousePacket): void {
|
|
|
825
381
|
} else if (overHint) {
|
|
826
382
|
target = { kind: "tool", component, view, section };
|
|
827
383
|
}
|
|
384
|
+
} else if (isCompactAssistantComponent(component)) {
|
|
385
|
+
// compact 摘要行:折叠时仅提示文字高亮,展开卡整体高亮。
|
|
386
|
+
if (component.expanded || overHint) target = { kind: "assistant", component };
|
|
828
387
|
}
|
|
829
388
|
}
|
|
830
389
|
}
|
|
831
390
|
applyFullscreenHover(tui, target);
|
|
832
391
|
}
|
|
833
392
|
|
|
834
|
-
/** 悬停状态变化才触发渲染(motion 事件密集,状态不变跳过)。 */
|
|
835
|
-
function applyFullscreenHover(tui: any, target: FullscreenHoverTarget | null): void {
|
|
836
|
-
let changed = false;
|
|
837
|
-
const nextCallId =
|
|
838
|
-
target?.kind === "tool" && !target.component.expanded
|
|
839
|
-
? (target.component.toolCallId ?? null)
|
|
840
|
-
: null;
|
|
841
|
-
if (nextCallId !== sharedToolHoverState().toolCallId) {
|
|
842
|
-
setHoveredToolCallId(nextCallId);
|
|
843
|
-
changed = true;
|
|
844
|
-
}
|
|
845
|
-
const nextGroup = target?.kind === "group" ? target.component : null;
|
|
846
|
-
if (setHoveredToolGroup(nextGroup)) changed = true;
|
|
847
|
-
const nextView = target?.kind === "tool" ? target.view : null;
|
|
848
|
-
const nextSection = target?.kind === "tool" ? target.section : null;
|
|
849
|
-
if (setHoveredToolIo(nextView, nextSection)) changed = true;
|
|
850
|
-
const nextButton = target?.kind === "button";
|
|
851
|
-
if (nextButton !== scrollButtonHovered) {
|
|
852
|
-
scrollButtonHovered = nextButton;
|
|
853
|
-
changed = true;
|
|
854
|
-
}
|
|
855
|
-
if (changed) tui.requestRender?.();
|
|
856
|
-
}
|
|
857
|
-
|
|
858
393
|
/**
|
|
859
394
|
* 实例级包装 TuiAltScreen.handleViewportInput(惰性 Proxy 安全):
|
|
860
395
|
* 原型方法取 original(绕开 proxy 函数包装),实例 own property 装 wrapper
|
|
@@ -1245,6 +780,7 @@ export function teardownToolMouseInteraction(
|
|
|
1245
780
|
setHoveredToolCallId(null);
|
|
1246
781
|
setHoveredToolGroup(null);
|
|
1247
782
|
setHoveredToolIo(null, null);
|
|
783
|
+
setHoveredCompactAssistant(null);
|
|
1248
784
|
try {
|
|
1249
785
|
if (isLazyProxyTui(toolMouseTui)) releaseFullscreenToolMouseMotion(toolMouseTui);
|
|
1250
786
|
else toolMouseTui?.terminal?.write?.(TOOL_MOUSE_DISABLE);
|
|
@@ -1258,11 +794,8 @@ export function teardownToolMouseInteraction(
|
|
|
1258
794
|
}
|
|
1259
795
|
restoreToolMouseRenderPatch();
|
|
1260
796
|
restoreFullscreenViewportInput(toolMouseTui);
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
scrollButtonWidget = null;
|
|
1264
|
-
scrollButtonSyncScheduled = false;
|
|
1265
|
-
toolMouseTui = null;
|
|
797
|
+
resetScrollButtonState();
|
|
798
|
+
setToolMouseTui(null);
|
|
1266
799
|
toolMouseUi = null;
|
|
1267
800
|
if (host[TOOL_MOUSE_OWNER_KEY] === owner) delete host[TOOL_MOUSE_OWNER_KEY];
|
|
1268
801
|
if (toolMouseInstallationOwner === owner) toolMouseInstallationOwner = null;
|
|
@@ -1271,8 +804,9 @@ export function teardownToolMouseInteraction(
|
|
|
1271
804
|
/** off 模式清理:清空 hover 与回到底部按钮状态(跨模块 rebind 统一经由此函数)。 */
|
|
1272
805
|
export function resetToolHoverState(): void {
|
|
1273
806
|
setHoveredToolCallId(null);
|
|
1274
|
-
|
|
1275
|
-
|
|
807
|
+
setHoveredCompactAssistant(null);
|
|
808
|
+
setScrollButtonVisible(false);
|
|
809
|
+
setScrollButtonHovered(false);
|
|
1276
810
|
releaseFullscreenToolMouseMotion(toolMouseTui);
|
|
1277
811
|
}
|
|
1278
812
|
|
|
@@ -1292,19 +826,19 @@ export function installToolMouseInteraction(
|
|
|
1292
826
|
// 0.84+ 的 tui 是惰性 Proxy:regular 保留原生 scrollback;fullscreen
|
|
1293
827
|
// 由官方 LayoutFrame 命中,并由扩展补齐 hover 所需的 all-motion 上报。
|
|
1294
828
|
ctx.ui.setWidget(TOOL_MOUSE_WIDGET_KEY, (tui: any, theme: any) => {
|
|
1295
|
-
|
|
829
|
+
setToolMouseTui(tui);
|
|
1296
830
|
setToolTuiFullscreen(fullscreenLazyTui(tui));
|
|
1297
831
|
if (isLazyProxyTui(tui)) {
|
|
1298
832
|
patchFullscreenViewportInput(tui);
|
|
1299
833
|
ensureFullscreenToolMouseMotion(tui);
|
|
1300
|
-
|
|
834
|
+
setScrollButtonWidget({
|
|
1301
835
|
render: (width: number) => {
|
|
1302
836
|
patchFullscreenViewportInput(tui);
|
|
1303
837
|
ensureFullscreenToolMouseMotion(tui);
|
|
1304
838
|
return renderScrollButton(width, theme);
|
|
1305
839
|
},
|
|
1306
840
|
invalidate() {},
|
|
1307
|
-
};
|
|
841
|
+
});
|
|
1308
842
|
return scrollButtonWidget;
|
|
1309
843
|
}
|
|
1310
844
|
// Wrap doRender to capture the live frame for tool click/hover mapping.
|
|
@@ -1314,7 +848,7 @@ export function installToolMouseInteraction(
|
|
|
1314
848
|
render: (width: number) => renderScrollButton(width, theme),
|
|
1315
849
|
invalidate() {},
|
|
1316
850
|
};
|
|
1317
|
-
|
|
851
|
+
setScrollButtonWidget(widget);
|
|
1318
852
|
return widget;
|
|
1319
853
|
});
|
|
1320
854
|
toolMouseInputUnsubscribe = ctx.ui.onTerminalInput(handleToolMouseInput);
|
|
@@ -1342,3 +876,11 @@ export function scheduleSessionRender(refresh?: () => void): void {
|
|
|
1342
876
|
tui.requestRender(true);
|
|
1343
877
|
}, 0);
|
|
1344
878
|
}
|
|
879
|
+
|
|
880
|
+
export { toolMouseTui } from "./mouse-scroll.ts";
|
|
881
|
+
export {
|
|
882
|
+
hoveredToolCallId,
|
|
883
|
+
isToolCallHovered,
|
|
884
|
+
setHoveredToolGroup,
|
|
885
|
+
setHoveredToolIo,
|
|
886
|
+
} from "./mouse-hover.ts";
|