pi-cc-extensions 0.9.4 → 0.9.6
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 +4 -4
- package/README.md +4 -4
- package/extensions/config/config.ts +2 -3
- package/extensions/config/panel.ts +2 -3
- package/extensions/feature/compact-thinking.ts +1 -1
- package/extensions/feature/reference/index.ts +23 -36
- package/extensions/feature/reference/session.ts +95 -1
- package/extensions/feature/reference/subagent.ts +60 -21
- package/extensions/renderer/compact-mode.ts +174 -62
- package/extensions/renderer/default-mode.ts +36 -39
- package/extensions/renderer/index.ts +5 -3
- package/extensions/renderer/mouse/interaction.ts +251 -80
- package/extensions/renderer/mouse/layout.ts +14 -5
- package/extensions/renderer/tool/diff/diff-component.ts +19 -2
- package/extensions/renderer/tool/diff/diff-edit-render.ts +18 -9
- package/extensions/renderer/tool/diff/diff-limits.ts +22 -21
- package/extensions/renderer/tool/diff/diff-write-render.ts +28 -18
- package/extensions/renderer/tool/grouping.ts +21 -5
- package/extensions/renderer/tool/mcp-title.ts +39 -0
- package/extensions/renderer/tool/names.ts +103 -1
- package/extensions/renderer/tool/result.ts +6 -1
- package/extensions/renderer/tool/show-more-hint.ts +10 -0
- package/package.json +2 -1
|
@@ -1,12 +1,21 @@
|
|
|
1
|
+
import { ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import { hasActiveTextPreview, showTextPreview } from "../../feature/context.ts";
|
|
2
3
|
import { ThinkingPreviewBlock } from "../../feature/compact-thinking.ts";
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
patchRegistry,
|
|
6
|
+
TOOL_GROUPING_PARENT_KEY,
|
|
7
|
+
TOOL_MOUSE_OWNER_KEY,
|
|
8
|
+
} from "../../utils/patch-keys.ts";
|
|
4
9
|
import { ToolGroupComponent } from "../tool/grouping.ts";
|
|
5
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
isCompactAssistantComponent,
|
|
12
|
+
markCompactRoundToolExpanded,
|
|
13
|
+
setHoveredCompactAssistant,
|
|
14
|
+
} from "../compact-mode.ts";
|
|
6
15
|
import { isMessageDisplayComponent } from "../tool/message-display.ts";
|
|
7
16
|
import { config } from "../../config/config.ts";
|
|
8
17
|
import { isLazyProxyTui } from "../../utils/fullscreen-detect.ts";
|
|
9
|
-
import { setToolTuiFullscreen } from "../tool/show-more-hint.ts";
|
|
18
|
+
import { setToolTuiFullscreen, isCollapseHintLine } from "../tool/show-more-hint.ts";
|
|
10
19
|
import {
|
|
11
20
|
type ExpandedToolIoView,
|
|
12
21
|
getActiveIoViewFrame,
|
|
@@ -75,7 +84,7 @@ type FrameToolRender = {
|
|
|
75
84
|
};
|
|
76
85
|
|
|
77
86
|
type InteractionRegion = {
|
|
78
|
-
kind: "collapsed-hint" | "expanded-card" | "show-more" | "scroll-bottom";
|
|
87
|
+
kind: "collapsed-hint" | "collapse-hint" | "expanded-card" | "show-more" | "scroll-bottom";
|
|
79
88
|
row: number;
|
|
80
89
|
startCol: number;
|
|
81
90
|
endCol: number;
|
|
@@ -118,6 +127,7 @@ function interactionRegionAt(packet: SgrMousePacket): InteractionRegion | null {
|
|
|
118
127
|
matches.find((region) => region.kind === "show-more") ??
|
|
119
128
|
matches.find((region) => region.kind === "scroll-bottom") ??
|
|
120
129
|
matches.find((region) => region.kind === "collapsed-hint") ??
|
|
130
|
+
matches.find((region) => region.kind === "collapse-hint") ??
|
|
121
131
|
matches.find((region) => region.kind === "expanded-card") ??
|
|
122
132
|
null
|
|
123
133
|
);
|
|
@@ -145,7 +155,12 @@ function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
|
|
|
145
155
|
const scrollButtonChanged = setScrollButtonHovered(nextScrollButtonHovered);
|
|
146
156
|
const component = region?.component;
|
|
147
157
|
const nextToolCallId = region?.kind === "collapsed-hint" ? (component?.toolCallId ?? null) : null;
|
|
148
|
-
|
|
158
|
+
// group 只在提示文字上高亮(展开卡整行都是 expanded-card,不能算 hint)。
|
|
159
|
+
const nextGroup =
|
|
160
|
+
(region?.kind === "collapsed-hint" || region?.kind === "collapse-hint") &&
|
|
161
|
+
component instanceof ToolGroupComponent
|
|
162
|
+
? component
|
|
163
|
+
: null;
|
|
149
164
|
const nextIoView = region?.kind === "show-more" ? (region.view ?? null) : null;
|
|
150
165
|
const nextIoSection = region?.kind === "show-more" ? (region.section ?? null) : null;
|
|
151
166
|
const changed = nextToolCallId !== sharedToolHoverState().toolCallId;
|
|
@@ -159,75 +174,92 @@ function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
|
|
|
159
174
|
tui.requestRender?.();
|
|
160
175
|
}
|
|
161
176
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
177
|
+
/** 单击判定:按下与松开落在同一格(renderer 合成 click 的条件),位移与时限各留容差。 */
|
|
178
|
+
const COLLAPSE_CLICK_MOVE_TOLERANCE = 1;
|
|
179
|
+
const COLLAPSE_CLICK_MAX_MS = 600;
|
|
180
|
+
/** 扩展收起自有卡后,官方 click 可能对同一次点击再 toggle 内部工具卡,短暂吞掉。 */
|
|
181
|
+
const SUPPRESS_OFFICIAL_CARD_CLICK_MS = 300;
|
|
182
|
+
/** 官方 fullscreen 的焦点事件串:发给原 handler 可清掉选区锚点(见 tui-alt-screen)。 */
|
|
183
|
+
const FULLSCREEN_FOCUS_OUT = "\x1b[O";
|
|
168
184
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
185
|
+
let pendingCollapsePress: {
|
|
186
|
+
card: any;
|
|
187
|
+
/** 命中的最内层组件:官方工具卡由 MouseRegion 的 click 收起。 */
|
|
188
|
+
component: any;
|
|
189
|
+
col: number;
|
|
190
|
+
row: number;
|
|
191
|
+
at: number;
|
|
192
|
+
} | null = null;
|
|
193
|
+
let suppressOfficialCardClickUntil = 0;
|
|
174
194
|
|
|
175
|
-
function
|
|
176
|
-
|
|
177
|
-
const id = expandPanelIdentity(card);
|
|
178
|
-
const prev = lastExpandClick;
|
|
179
|
-
pendingExpandPress = { id };
|
|
180
|
-
if (prev && prev.id === id && now - prev.at <= EXPAND_PANEL_DOUBLE_CLICK_MS) {
|
|
181
|
-
pendingExpandPress = null;
|
|
182
|
-
lastExpandClick = null;
|
|
183
|
-
return true;
|
|
184
|
-
}
|
|
185
|
-
return false;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function completeExpandPanelClick(): void {
|
|
189
|
-
if (!pendingExpandPress) return;
|
|
190
|
-
const click = { id: pendingExpandPress.id, at: Date.now() };
|
|
191
|
-
pendingExpandPress = null;
|
|
192
|
-
if (armExpandClickTimer) clearTimeout(armExpandClickTimer);
|
|
193
|
-
armExpandClickTimer = setTimeout(() => {
|
|
194
|
-
armExpandClickTimer = null;
|
|
195
|
-
// 50ms 内又按下:仍是同一次单击,不能武装成双击。
|
|
196
|
-
if (pendingExpandPress) return;
|
|
197
|
-
lastExpandClick = click;
|
|
198
|
-
}, EXPAND_PANEL_CLICK_ARM_MS);
|
|
199
|
-
if (
|
|
200
|
-
typeof armExpandClickTimer === "object" &&
|
|
201
|
-
armExpandClickTimer &&
|
|
202
|
-
"unref" in armExpandClickTimer
|
|
203
|
-
) {
|
|
204
|
-
armExpandClickTimer.unref();
|
|
205
|
-
}
|
|
195
|
+
function clearPendingCollapsePress(): void {
|
|
196
|
+
pendingCollapsePress = null;
|
|
206
197
|
}
|
|
207
198
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
if (
|
|
212
|
-
|
|
213
|
-
|
|
199
|
+
/** 带键拖动或滚轮:位置真的变了(超出抖动容差)就不再是单击。 */
|
|
200
|
+
function clearPendingCollapsePressOnMove(packet: SgrMousePacket): void {
|
|
201
|
+
const press = pendingCollapsePress;
|
|
202
|
+
if (!press) return;
|
|
203
|
+
if ((packet.code & 64) !== 0) {
|
|
204
|
+
pendingCollapsePress = null;
|
|
205
|
+
return;
|
|
214
206
|
}
|
|
207
|
+
if (Math.abs(packet.col - press.col) <= COLLAPSE_CLICK_MOVE_TOLERANCE) {
|
|
208
|
+
if (Math.abs(packet.row - press.row) <= COLLAPSE_CLICK_MOVE_TOLERANCE) return;
|
|
209
|
+
}
|
|
210
|
+
pendingCollapsePress = null;
|
|
215
211
|
}
|
|
216
212
|
|
|
217
|
-
function
|
|
218
|
-
// 单击也 consume,避免官方链再合成一次 press 把单击当成双击。
|
|
219
|
-
if (!isExpandPanelDoubleClick(card)) return true;
|
|
220
|
-
card.setExpanded(false);
|
|
213
|
+
function clearHoverState(): void {
|
|
221
214
|
setHoveredToolCallId(null);
|
|
222
215
|
setHoveredToolGroup(null);
|
|
223
216
|
setHoveredThinking(null);
|
|
224
217
|
setHoveredMessageDisplay(null);
|
|
225
218
|
setHoveredToolIo(null, null);
|
|
226
219
|
setHoveredCompactAssistant(null);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** 收起一张展开卡(group 的 setExpanded 会传播到内部工具)。 */
|
|
223
|
+
function collapseExpandedCard(tui: any, card: any): void {
|
|
224
|
+
card.setExpanded(false);
|
|
225
|
+
clearHoverState();
|
|
227
226
|
card.invalidate?.();
|
|
228
|
-
tui
|
|
229
|
-
|
|
230
|
-
|
|
227
|
+
tui?.requestRender?.();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function rememberCollapsePress(card: any, component: any, packet: SgrMousePacket): void {
|
|
231
|
+
pendingCollapsePress = { card, component, col: packet.col, row: packet.row, at: Date.now() };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 松手结算:只有"按下与松开在同一格"的完整单击才收起,拖动选择文本时保持展开。
|
|
236
|
+
* 官方工具卡(result 区的 MouseRegion)由 guard 在官方 click 里收起,这里必须跳过,
|
|
237
|
+
* 否则先收起会被官方 click 的 setExpanded(!expanded) 翻回来。
|
|
238
|
+
*/
|
|
239
|
+
function resolveCollapsePress(
|
|
240
|
+
tui: any,
|
|
241
|
+
packet: SgrMousePacket,
|
|
242
|
+
options: { skipOfficialCards: boolean },
|
|
243
|
+
): any {
|
|
244
|
+
const press = pendingCollapsePress;
|
|
245
|
+
pendingCollapsePress = null;
|
|
246
|
+
if (!press || packet.final !== "m") return undefined;
|
|
247
|
+
if (Math.abs(packet.col - press.col) > COLLAPSE_CLICK_MOVE_TOLERANCE) return undefined;
|
|
248
|
+
if (Math.abs(packet.row - press.row) > COLLAPSE_CLICK_MOVE_TOLERANCE) return undefined;
|
|
249
|
+
if (Date.now() - press.at > COLLAPSE_CLICK_MAX_MS) return undefined;
|
|
250
|
+
if (options.skipOfficialCards && press.component instanceof ToolExecutionComponent)
|
|
251
|
+
return undefined;
|
|
252
|
+
const card = press.card;
|
|
253
|
+
if (
|
|
254
|
+
(card instanceof ToolGroupComponent || isCompactAssistantComponent(card)) &&
|
|
255
|
+
press.component !== card
|
|
256
|
+
) {
|
|
257
|
+
// 点击落在卡内工具行:官方 click 会对同一次点击再次 toggle,吞掉它。
|
|
258
|
+
suppressOfficialCardClickUntil = Date.now() + SUPPRESS_OFFICIAL_CARD_CLICK_MS;
|
|
259
|
+
}
|
|
260
|
+
collapseExpandedCard(tui, card);
|
|
261
|
+
// 返回收起的卡:调用方只对 compact 面板清官方选区。
|
|
262
|
+
return card;
|
|
231
263
|
}
|
|
232
264
|
|
|
233
265
|
function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
|
|
@@ -237,9 +269,13 @@ function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
237
269
|
if (region.kind === "show-more") return tryOpenToolIoShowMore(region);
|
|
238
270
|
const component = region.component;
|
|
239
271
|
if (!component) return false;
|
|
240
|
-
if (region.kind === "expanded-card"
|
|
272
|
+
if (region.kind === "expanded-card" || region.kind === "collapse-hint") {
|
|
273
|
+
// regular 模式没有官方 click 合成,按下先记账,松手结算是否收起。
|
|
274
|
+
rememberCollapsePress(component, component, packet);
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
241
277
|
component.setExpanded(true);
|
|
242
|
-
|
|
278
|
+
clearPendingCollapsePress();
|
|
243
279
|
setHoveredToolCallId(null);
|
|
244
280
|
setHoveredToolGroup(null);
|
|
245
281
|
setHoveredToolIo(null, null);
|
|
@@ -311,11 +347,37 @@ function releaseFullscreenToolMouseMotion(tui?: any): void {
|
|
|
311
347
|
const FULLSCREEN_VIEWPORT_PATCH = Symbol("ccstyle.fullscreen-viewport-patch");
|
|
312
348
|
const FULLSCREEN_WHEEL_SCROLL_ORIGINAL = Symbol("ccstyle.fullscreen-wheel-scroll-original");
|
|
313
349
|
|
|
350
|
+
/**
|
|
351
|
+
* diff 结果组件自带 remainder 行:声明存在时只有那一行是展开入口(正文里的同名字样不算);
|
|
352
|
+
* 未声明的组件继续用文本规则。
|
|
353
|
+
*/
|
|
354
|
+
function isCollapsedHintRow(component: any, finalLine: string): boolean {
|
|
355
|
+
const result = component?.resultRendererComponent;
|
|
356
|
+
return typeof result?.isCollapsedHintLine === "function"
|
|
357
|
+
? result.isCollapsedHintLine(finalLine)
|
|
358
|
+
: true;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* pi 0.87 给工具结果区套了 MouseRegion:整卡左键 click 都会 setExpanded。
|
|
363
|
+
* 命中该区域时官方会跳过文本选区,所以可以安全吞掉;单行摘要卡没有声明
|
|
364
|
+
* remainder 行,继续走官方整行行为。
|
|
365
|
+
*/
|
|
366
|
+
function blocksOfficialCardToggle(component: any, event: any): boolean {
|
|
367
|
+
if (event?.type !== "click" || event.button !== "left" || component?.expanded) return false;
|
|
368
|
+
const result = component?.resultRendererComponent;
|
|
369
|
+
if (typeof result?.isCollapsedHintLine !== "function") return false;
|
|
370
|
+
const lines = component.render?.(Math.max(1, Math.floor(Number(event.width) || 0)));
|
|
371
|
+
const line = Array.isArray(lines) ? lines[event.y] : undefined;
|
|
372
|
+
return typeof line === "string" && !result.isCollapsedHintLine(line);
|
|
373
|
+
}
|
|
374
|
+
|
|
314
375
|
/**
|
|
315
376
|
* 官方 fullscreen 工具卡点击:collapsed hint 单击展开
|
|
316
|
-
* (有且仅保持一个展开:展开前收起其他工具卡),expanded
|
|
317
|
-
*
|
|
318
|
-
*
|
|
377
|
+
* (有且仅保持一个展开:展开前收起其他工具卡),expanded 卡放行官方 press,
|
|
378
|
+
* 位置不变的松手才收起(官方卡经 guard,思考/group/compact 自有卡由扩展结算),
|
|
379
|
+
* 拖动选择文本时不收起;截断头 show-more 单击打开全量预览;回到底部按钮 scrollToBottom。
|
|
380
|
+
* 滚动条列、含 OSC8 链接行、非工具区域、折叠卡正文放行官方。
|
|
319
381
|
*/
|
|
320
382
|
function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
321
383
|
const layout = tui.currentLayout;
|
|
@@ -333,7 +395,10 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
333
395
|
const target = componentAtLocalRow(hit.box.component, hit.localRow, contentWidth);
|
|
334
396
|
if (!target) return false;
|
|
335
397
|
const component = target.component;
|
|
336
|
-
|
|
398
|
+
// compact 展开面板内命中时 owner 是面板:收起对象与记账对象都归面板,
|
|
399
|
+
// 否则 skipOfficialCards 会把内部工具当成官方卡跳过。
|
|
400
|
+
const card = target.group ?? target.owner ?? component;
|
|
401
|
+
const pressComponent = target.owner ?? component;
|
|
337
402
|
// 回到底部按钮:按组件引用命中,不依赖渲染行缓存。
|
|
338
403
|
if (getScrollButtonVisible() && component === getScrollButtonWidget()) {
|
|
339
404
|
tui.scrollToBottom?.();
|
|
@@ -351,20 +416,32 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
351
416
|
if (!component.expanded) {
|
|
352
417
|
// collapsed 仅按钮文本可展开,不能把同一行正文/留白变成点击区。
|
|
353
418
|
const hint = collapsedHintHitbox(line);
|
|
354
|
-
|
|
419
|
+
const onHint = Boolean(
|
|
420
|
+
hint &&
|
|
421
|
+
packet.col >= hint.startCol &&
|
|
422
|
+
packet.col <= hint.endCol &&
|
|
423
|
+
isCollapsedHintRow(component, line),
|
|
424
|
+
);
|
|
425
|
+
if (!onHint) {
|
|
426
|
+
// 面板内非提示区(工具卡标题/摘要行、thinking 预览正文):单击收起整块面板。
|
|
427
|
+
if (target.owner) rememberCollapsePress(target.owner, target.owner, packet);
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
355
430
|
// single-expand:展开前收起其他已展开工具卡/group。
|
|
356
431
|
const others: any[] = [];
|
|
357
432
|
collectFullscreenToolCards(hit.box.component, others);
|
|
358
433
|
for (const other of others) {
|
|
359
434
|
if (other !== component && other.expanded) {
|
|
360
|
-
// 展开 round
|
|
361
|
-
if (isThinking && isCompactAssistantComponent(other)) continue;
|
|
435
|
+
// 展开 round 卡内 thinking/工具时,外层 compact 卡是它的容器,不能收起。
|
|
436
|
+
if ((isThinking || isTool) && isCompactAssistantComponent(other)) continue;
|
|
362
437
|
other.setExpanded(false);
|
|
363
438
|
other.invalidate?.();
|
|
364
439
|
}
|
|
365
440
|
}
|
|
441
|
+
// 展开 round 卡内工具时,让 compact 的强制折叠放行它(非 round 内工具为空操作)。
|
|
442
|
+
if (isTool) markCompactRoundToolExpanded(component);
|
|
366
443
|
component.setExpanded(true);
|
|
367
|
-
|
|
444
|
+
clearPendingCollapsePress();
|
|
368
445
|
} else {
|
|
369
446
|
// 普通工具截断头 show-more:打开全量预览(不收起)。
|
|
370
447
|
const view = isTool ? component.resultRendererComponent : null;
|
|
@@ -372,7 +449,7 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
372
449
|
const plain = stripTerminalSequencesPreservingLayout(line);
|
|
373
450
|
const section = view.matchShowMoreLine(plain);
|
|
374
451
|
if (section) {
|
|
375
|
-
|
|
452
|
+
clearPendingCollapsePress();
|
|
376
453
|
const box = view.showMoreHitbox(plain);
|
|
377
454
|
return tryOpenToolIoShowMore({
|
|
378
455
|
kind: "show-more",
|
|
@@ -385,17 +462,20 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
385
462
|
});
|
|
386
463
|
}
|
|
387
464
|
}
|
|
388
|
-
//
|
|
389
|
-
|
|
465
|
+
// 其余放行官方:选区、OSC8 链接与 click 合成都交给 renderer。
|
|
466
|
+
// 完整单击的收起:官方卡由 guard 处理,自有卡在松手时结算。
|
|
467
|
+
rememberCollapsePress(card, pressComponent, packet);
|
|
468
|
+
return false;
|
|
390
469
|
}
|
|
391
|
-
// 点击后清 hover
|
|
470
|
+
// 点击后清 hover 高亮。只 invalidate 点中的组件(或它所在的 group),
|
|
471
|
+
// 不要动 owner 面板:面板 invalidate 会连带重跑 updateContent,重建 thinking 实例。
|
|
392
472
|
setHoveredToolCallId(null);
|
|
393
473
|
setHoveredToolGroup(null);
|
|
394
474
|
setHoveredThinking(null);
|
|
395
475
|
setHoveredMessageDisplay(null);
|
|
396
476
|
setHoveredToolIo(null, null);
|
|
397
477
|
setHoveredCompactAssistant(null);
|
|
398
|
-
|
|
478
|
+
(target.group ?? component).invalidate?.();
|
|
399
479
|
tui.requestRender?.();
|
|
400
480
|
return true;
|
|
401
481
|
}
|
|
@@ -499,7 +579,22 @@ function patchFullscreenViewportInput(tui: any): void {
|
|
|
499
579
|
if (packets && tui.hasOverlay?.() && hasActiveTextPreview()) return undefined;
|
|
500
580
|
if (packets && !tui.hasOverlay?.()) {
|
|
501
581
|
for (const packet of packets) {
|
|
502
|
-
if (isSgrLeftRelease(packet))
|
|
582
|
+
if (isSgrLeftRelease(packet)) {
|
|
583
|
+
// 只给 compact 面板清选区:自有面板收起会重排布局,官方选区锚点
|
|
584
|
+
// 还停在旧布局上,同一次 release 会结算出多行高亮。
|
|
585
|
+
if (
|
|
586
|
+
isCompactAssistantComponent(
|
|
587
|
+
resolveCollapsePress(tui, packet, { skipOfficialCards: true }),
|
|
588
|
+
)
|
|
589
|
+
) {
|
|
590
|
+
try {
|
|
591
|
+
Reflect.apply(original, this, [FULLSCREEN_FOCUS_OUT]);
|
|
592
|
+
} catch {
|
|
593
|
+
/* 老版本没有选区状态 */
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
} else if (!isSgrLeftPress(packet) && !isSgrIdleMotion(packet))
|
|
597
|
+
clearPendingCollapsePressOnMove(packet);
|
|
503
598
|
if (isSgrLeftPress(packet) && handleFullscreenToolClick(tui, packet)) {
|
|
504
599
|
return { consume: true };
|
|
505
600
|
}
|
|
@@ -574,7 +669,11 @@ function buildInteractionFrame(
|
|
|
574
669
|
const line = placement.finalLine;
|
|
575
670
|
if (!component.expanded) {
|
|
576
671
|
const box = collapsedHintHitbox(line);
|
|
577
|
-
if (
|
|
672
|
+
if (
|
|
673
|
+
box &&
|
|
674
|
+
COLLAPSED_TOOL_SUMMARY.test(stripTerminalSequences(line)) &&
|
|
675
|
+
isCollapsedHintRow(component, line)
|
|
676
|
+
) {
|
|
578
677
|
regions.push({ kind: "collapsed-hint", row: finalRow, ...box, component });
|
|
579
678
|
}
|
|
580
679
|
continue;
|
|
@@ -608,6 +707,11 @@ function buildInteractionFrame(
|
|
|
608
707
|
if (placement.componentRow < cardStart) continue;
|
|
609
708
|
const finalRow = lineIndexToScreenRow(placement.lineIndex);
|
|
610
709
|
if (finalRow >= 1 && finalRow <= visibleRows) {
|
|
710
|
+
// 展开态的 ↑ Collapse 提示单独成区:hover 高亮 hint,点击仍收起整卡。
|
|
711
|
+
const hintBox = collapsedHintHitbox(placement.finalLine);
|
|
712
|
+
if (hintBox && isCollapseHintLine(stripTerminalSequences(placement.finalLine))) {
|
|
713
|
+
regions.push({ kind: "collapse-hint", row: finalRow, ...hintBox, component });
|
|
714
|
+
}
|
|
611
715
|
regions.push({
|
|
612
716
|
kind: "expanded-card",
|
|
613
717
|
row: finalRow,
|
|
@@ -837,8 +941,15 @@ function handleToolMouseInput(data: string): { consume: true } | undefined {
|
|
|
837
941
|
let consumed = false;
|
|
838
942
|
for (const packet of packets) {
|
|
839
943
|
updateToolSummaryHover(getToolMouseTui(), packet);
|
|
840
|
-
if (isSgrLeftRelease(packet))
|
|
841
|
-
|
|
944
|
+
if (isSgrLeftRelease(packet)) {
|
|
945
|
+
resolveCollapsePress(getToolMouseTui(), packet, { skipOfficialCards: false });
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
if (!isSgrLeftPress(packet)) {
|
|
949
|
+
// 带键拖动或滚轮:位置变了就不再是单击。
|
|
950
|
+
if (!isSgrIdleMotion(packet)) clearPendingCollapsePressOnMove(packet);
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
842
953
|
if (toggleToolAtMouseClick(getToolMouseTui(), packet)) {
|
|
843
954
|
consumed = true;
|
|
844
955
|
}
|
|
@@ -867,7 +978,9 @@ export function teardownToolMouseInteraction(
|
|
|
867
978
|
setHoveredMessageDisplay(null);
|
|
868
979
|
setHoveredToolIo(null, null);
|
|
869
980
|
setHoveredCompactAssistant(null);
|
|
870
|
-
|
|
981
|
+
releaseToolCardMouseGuard();
|
|
982
|
+
clearPendingCollapsePress();
|
|
983
|
+
suppressOfficialCardClickUntil = 0;
|
|
871
984
|
try {
|
|
872
985
|
if (isLazyProxyTui(getToolMouseTui())) releaseFullscreenToolMouseMotion(getToolMouseTui());
|
|
873
986
|
else getToolMouseTui()?.terminal?.write?.(TOOL_MOUSE_DISABLE);
|
|
@@ -901,6 +1014,63 @@ export function resetToolHoverState(): void {
|
|
|
901
1014
|
releaseFullscreenToolMouseMotion(getToolMouseTui());
|
|
902
1015
|
}
|
|
903
1016
|
|
|
1017
|
+
/**
|
|
1018
|
+
* pi 0.87 起工具卡结果区自带 MouseRegion,整卡左键 click 都会 setExpanded。
|
|
1019
|
+
* ccstyle 的多行折叠卡(rich diff)只允许声明的 remainder 行展开,
|
|
1020
|
+
* 其余位置直接吞掉 click,避免官方把 diff 正文当展开入口。
|
|
1021
|
+
* 0.84 及更早没有 handleMouse,安装时跳过。
|
|
1022
|
+
*/
|
|
1023
|
+
const TOOL_CARD_MOUSE_GUARD_KEY = Symbol.for("pi.ccstyle.tool-card-mouse-guard");
|
|
1024
|
+
|
|
1025
|
+
type ToolCardMouseGuard = {
|
|
1026
|
+
prototype: any;
|
|
1027
|
+
original: (event: any) => any;
|
|
1028
|
+
wrapper: (event: any) => any;
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
/** 官方 click 落在展开卡上:同一 group 的任意内部工具行都收起整个 group。 */
|
|
1032
|
+
function collapseExpandedCardFromOfficialClick(component: any): void {
|
|
1033
|
+
if (typeof component?.setExpanded !== "function") return;
|
|
1034
|
+
const group = component?.[TOOL_GROUPING_PARENT_KEY];
|
|
1035
|
+
collapseExpandedCard(getToolMouseTui(), group instanceof ToolGroupComponent ? group : component);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function installToolCardMouseGuard(): void {
|
|
1039
|
+
const prototype = (ToolExecutionComponent as any)?.prototype;
|
|
1040
|
+
if (typeof prototype?.handleMouse !== "function") return;
|
|
1041
|
+
const previous = (globalThis as any)[TOOL_CARD_MOUSE_GUARD_KEY] as ToolCardMouseGuard | undefined;
|
|
1042
|
+
if (previous && previous.prototype === prototype && prototype.handleMouse === previous.wrapper)
|
|
1043
|
+
return;
|
|
1044
|
+
// 每次取当前值作为下游,可能是别的扩展的包装。
|
|
1045
|
+
const original = prototype.handleMouse;
|
|
1046
|
+
const wrapper = function (this: any, event: any) {
|
|
1047
|
+
if (event?.type === "click" && event.button === "left") {
|
|
1048
|
+
// 扩展刚收起过自有卡:这次 click 属于同一次单击,吞掉避免内部工具被翻回展开。
|
|
1049
|
+
if (suppressOfficialCardClickUntil && Date.now() <= suppressOfficialCardClickUntil) {
|
|
1050
|
+
suppressOfficialCardClickUntil = 0;
|
|
1051
|
+
if (this?.expanded) collapseExpandedCardFromOfficialClick(this);
|
|
1052
|
+
return { handled: true };
|
|
1053
|
+
}
|
|
1054
|
+
// 展开卡整卡 click:接管官方 toggle,改为收起(group 内部工具行收起整个 group)。
|
|
1055
|
+
if (this?.expanded && typeof this.setExpanded === "function") {
|
|
1056
|
+
collapseExpandedCardFromOfficialClick(this);
|
|
1057
|
+
return { handled: true };
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
if (blocksOfficialCardToggle(this, event)) return { handled: true };
|
|
1061
|
+
return original.call(this, event);
|
|
1062
|
+
};
|
|
1063
|
+
prototype.handleMouse = wrapper;
|
|
1064
|
+
(globalThis as any)[TOOL_CARD_MOUSE_GUARD_KEY] = { prototype, original, wrapper };
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function releaseToolCardMouseGuard(): void {
|
|
1068
|
+
const guard = (globalThis as any)[TOOL_CARD_MOUSE_GUARD_KEY] as ToolCardMouseGuard | undefined;
|
|
1069
|
+
if (!guard) return;
|
|
1070
|
+
if (guard.prototype?.handleMouse === guard.wrapper) guard.prototype.handleMouse = guard.original;
|
|
1071
|
+
(globalThis as any)[TOOL_CARD_MOUSE_GUARD_KEY] = undefined;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
904
1074
|
export function installToolMouseInteraction(
|
|
905
1075
|
ctx: any,
|
|
906
1076
|
owner: object = DEFAULT_TOOL_MOUSE_OWNER,
|
|
@@ -912,6 +1082,7 @@ export function installToolMouseInteraction(
|
|
|
912
1082
|
|
|
913
1083
|
toolMouseInstallationOwner = owner;
|
|
914
1084
|
patchRegistry.install(TOOL_MOUSE_OWNER_KEY, owner);
|
|
1085
|
+
installToolCardMouseGuard();
|
|
915
1086
|
setHoveredToolCallId(null);
|
|
916
1087
|
toolMouseUi = ctx.ui;
|
|
917
1088
|
// 0.84+ 的 tui 是惰性 Proxy:regular 保留原生 scrollback;fullscreen
|
|
@@ -11,12 +11,17 @@ export type ComponentRowHit = {
|
|
|
11
11
|
row: number;
|
|
12
12
|
/** 内部工具命中时所属的展开 group;普通卡点击仍折叠整个 group。 */
|
|
13
13
|
group?: ToolGroupComponent;
|
|
14
|
+
/** compact 展开面板内的命中:外层面板,单击非提示区时收起它。 */
|
|
15
|
+
owner?: any;
|
|
14
16
|
};
|
|
15
17
|
|
|
16
|
-
/** 行内 [click to show more] 提示的命中列区间(1-based,含两端)。 */
|
|
18
|
+
/** 行内 [click to show more] / [↑ Collapse] 提示的命中列区间(1-based,含两端)。 */
|
|
17
19
|
export function collapsedHintHitbox(line: string): { startCol: number; endCol: number } | null {
|
|
18
20
|
const plain = stripTerminalSequencesPreservingLayout(line);
|
|
19
|
-
const match =
|
|
21
|
+
const match =
|
|
22
|
+
/(\([^()\n]* \/ click\)|click to show more|to show more|↑ Collapse|to collapse)(?=\)?\s*$)/.exec(
|
|
23
|
+
plain,
|
|
24
|
+
);
|
|
20
25
|
if (!match?.[1]) return null;
|
|
21
26
|
const startCol = visibleWidth(plain.slice(0, match.index)) + 1;
|
|
22
27
|
return { startCol, endCol: startCol + visibleWidth(match[1]) - 1 };
|
|
@@ -119,11 +124,15 @@ export function componentAtLocalRow(
|
|
|
119
124
|
return { component, row: localRow };
|
|
120
125
|
}
|
|
121
126
|
if (isCompactAssistantComponent(component)) {
|
|
122
|
-
//
|
|
127
|
+
// 折叠:整行摘要。展开:先命中面板内 thinking/工具卡/文本,其余(空行)仍归外层卡片。
|
|
123
128
|
if (component.expanded === true) {
|
|
124
129
|
const inner = nestedChildAtRow(component, localRow, width);
|
|
125
|
-
if (inner
|
|
126
|
-
|
|
130
|
+
if (inner) {
|
|
131
|
+
// 展开的 thinking 有自己的展开态:点它只收它自己,不收整块面板。
|
|
132
|
+
const ownExpandable = inner instanceof ThinkingPreviewBlock && inner.expanded === true;
|
|
133
|
+
return ownExpandable
|
|
134
|
+
? { component: inner, row: localRow }
|
|
135
|
+
: { component: inner, row: localRow, owner: component };
|
|
127
136
|
}
|
|
128
137
|
}
|
|
129
138
|
return { component, row: localRow };
|
|
@@ -3,8 +3,9 @@ import {
|
|
|
3
3
|
type DiffIndicatorMode,
|
|
4
4
|
type ToolDisplayConfig,
|
|
5
5
|
} from "../../../config/config.ts";
|
|
6
|
-
import
|
|
6
|
+
import { stripTerminalSequencesPreservingLayout } from "../../../utils/ansi-text.ts";
|
|
7
7
|
import { RICH_DIFF_COMPONENT } from "../../../utils/patch-keys.ts";
|
|
8
|
+
import type { DiffPresentationMode } from "./diff-presentation.ts";
|
|
8
9
|
export { RICH_DIFF_COMPONENT };
|
|
9
10
|
|
|
10
11
|
/** Snapshot or live getter — panel changes must apply on the next paint. */
|
|
@@ -33,6 +34,15 @@ export function resolveLiveDisplayConfig(input: DisplayConfigInput): ToolDisplay
|
|
|
33
34
|
return typeof input === "function" ? input() : input;
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
/**
|
|
38
|
+
* 比对用归一化:ANSI、缩进前缀和右侧填充不影响判断。
|
|
39
|
+
* 折叠态 remainder 行里有任意正文,入口必须先由组件声明再比对文本。
|
|
40
|
+
*/
|
|
41
|
+
export function normalizeCollapsedHintLine(line: string | undefined): string | undefined {
|
|
42
|
+
if (line === undefined) return undefined;
|
|
43
|
+
return stripTerminalSequencesPreservingLayout(line).replace(/\s+/g, " ").trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
/** Cache key fragment so indicator/wrap/limits invalidate without host recreate. */
|
|
37
47
|
export function displayConfigCacheKey(config: ToolDisplayConfig): string {
|
|
38
48
|
return [
|
|
@@ -42,7 +52,6 @@ export function displayConfigCacheKey(config: ToolDisplayConfig): string {
|
|
|
42
52
|
String(config.editDiffCollapsedLines),
|
|
43
53
|
String(config.writeDiffCollapsedLines),
|
|
44
54
|
config.diffWordWrap ? "1" : "0",
|
|
45
|
-
String(config.expandedPreviewMaxLines),
|
|
46
55
|
].join(":");
|
|
47
56
|
}
|
|
48
57
|
|
|
@@ -59,6 +68,8 @@ export function createDiffRenderCache() {
|
|
|
59
68
|
let cachedConfigKey: string | undefined;
|
|
60
69
|
let cachedHovered: boolean | undefined;
|
|
61
70
|
let cachedLines: string[] | undefined;
|
|
71
|
+
/** 与 cachedLines 同一次渲染的折叠态 remainder 行文本。 */
|
|
72
|
+
let cachedHintLine: string | undefined;
|
|
62
73
|
|
|
63
74
|
return {
|
|
64
75
|
get(
|
|
@@ -80,6 +91,9 @@ export function createDiffRenderCache() {
|
|
|
80
91
|
}
|
|
81
92
|
return undefined;
|
|
82
93
|
},
|
|
94
|
+
getHintLine(): string | undefined {
|
|
95
|
+
return cachedHintLine;
|
|
96
|
+
},
|
|
83
97
|
set(
|
|
84
98
|
width: number,
|
|
85
99
|
expanded: boolean,
|
|
@@ -87,6 +101,7 @@ export function createDiffRenderCache() {
|
|
|
87
101
|
configKey: string,
|
|
88
102
|
hovered: boolean,
|
|
89
103
|
lines: string[],
|
|
104
|
+
hintLine?: string,
|
|
90
105
|
): string[] {
|
|
91
106
|
cachedWidth = width;
|
|
92
107
|
cachedExpanded = expanded;
|
|
@@ -94,6 +109,7 @@ export function createDiffRenderCache() {
|
|
|
94
109
|
cachedConfigKey = configKey;
|
|
95
110
|
cachedHovered = hovered;
|
|
96
111
|
cachedLines = lines;
|
|
112
|
+
cachedHintLine = hintLine;
|
|
97
113
|
return lines;
|
|
98
114
|
},
|
|
99
115
|
invalidate(): void {
|
|
@@ -103,6 +119,7 @@ export function createDiffRenderCache() {
|
|
|
103
119
|
cachedConfigKey = undefined;
|
|
104
120
|
cachedHovered = undefined;
|
|
105
121
|
cachedLines = undefined;
|
|
122
|
+
cachedHintLine = undefined;
|
|
106
123
|
},
|
|
107
124
|
};
|
|
108
125
|
}
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
displayConfigCacheKey,
|
|
33
33
|
resolveDiffIndicatorMode,
|
|
34
34
|
resolveLiveDisplayConfig,
|
|
35
|
+
normalizeCollapsedHintLine,
|
|
35
36
|
type DiffRenderOptions,
|
|
36
37
|
type DisplayConfigInput,
|
|
37
38
|
} from "./diff-component.ts";
|
|
@@ -127,6 +128,11 @@ export function renderEditDiffResult(
|
|
|
127
128
|
|
|
128
129
|
return {
|
|
129
130
|
[RICH_DIFF_COMPONENT]: true,
|
|
131
|
+
/** 鼠标层只在真正的 remainder 行上提供展开入口。 */
|
|
132
|
+
isCollapsedHintLine(plainLine: string): boolean {
|
|
133
|
+
const hint = cache.getHintLine();
|
|
134
|
+
return hint !== undefined && normalizeCollapsedHintLine(plainLine) === hint;
|
|
135
|
+
},
|
|
130
136
|
render(width: number): string[] {
|
|
131
137
|
// Live config: panel can change indicator/wrap/limits after this component is created.
|
|
132
138
|
const live = resolveLiveDisplayConfig(config);
|
|
@@ -161,11 +167,7 @@ export function renderEditDiffResult(
|
|
|
161
167
|
}
|
|
162
168
|
|
|
163
169
|
const headerRows = renderHeaderRows(parsed.stats, mode, safeWidth, theme);
|
|
164
|
-
const displayLimit = resolveDiffDisplayLimit(
|
|
165
|
-
options.expanded,
|
|
166
|
-
live.editDiffCollapsedLines,
|
|
167
|
-
live.expandedPreviewMaxLines,
|
|
168
|
-
);
|
|
170
|
+
const displayLimit = resolveDiffDisplayLimit(options.expanded, live.editDiffCollapsedLines);
|
|
169
171
|
const processBudget = resolveDiffProcessBudget(displayLimit, wordWrap);
|
|
170
172
|
// Only highlight/render a prefix that can fill the display limit; full-diff
|
|
171
173
|
// LCS + syntax highlight on thousands of hidden lines is pure waste when collapsed.
|
|
@@ -198,7 +200,6 @@ export function renderEditDiffResult(
|
|
|
198
200
|
safeWidth,
|
|
199
201
|
options.expanded,
|
|
200
202
|
live.editDiffCollapsedLines,
|
|
201
|
-
live.expandedPreviewMaxLines,
|
|
202
203
|
parsed.stats.hunks,
|
|
203
204
|
theme,
|
|
204
205
|
unprocessedLogicalRows,
|
|
@@ -207,11 +208,19 @@ export function renderEditDiffResult(
|
|
|
207
208
|
const frame = renderDiffFrameLine(safeWidth, theme);
|
|
208
209
|
const renderedLines =
|
|
209
210
|
mode === "unified"
|
|
210
|
-
? [...headerRows.map((row) => row.text), frame, ...bodyWithLimit, frame]
|
|
211
|
-
: [...headerRows.map((row) => row.text), ...bodyWithLimit];
|
|
211
|
+
? [...headerRows.map((row) => row.text), frame, ...bodyWithLimit.lines, frame]
|
|
212
|
+
: [...headerRows.map((row) => row.text), ...bodyWithLimit.lines];
|
|
212
213
|
|
|
213
214
|
const clampedLines = clampDiffLinesToWidth(renderedLines, safeWidth);
|
|
214
|
-
return cache.set(
|
|
215
|
+
return cache.set(
|
|
216
|
+
safeWidth,
|
|
217
|
+
options.expanded,
|
|
218
|
+
mode,
|
|
219
|
+
configKey,
|
|
220
|
+
hovered,
|
|
221
|
+
clampedLines,
|
|
222
|
+
normalizeCollapsedHintLine(bodyWithLimit.hintLine),
|
|
223
|
+
);
|
|
215
224
|
},
|
|
216
225
|
invalidate: cache.invalidate,
|
|
217
226
|
} as Component;
|