pi-cc-extensions 1.0.3 → 1.0.4
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.
|
@@ -9,13 +9,14 @@ import {
|
|
|
9
9
|
getMarkdownTheme,
|
|
10
10
|
} from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { Key, Markdown, matchesKey, visibleWidth } from "@earendil-works/pi-tui";
|
|
12
|
-
import {
|
|
12
|
+
import { type Color, rgbColor } from "@earendil-works/pi-tui";
|
|
13
13
|
import { padLine } from "../utils/format.ts";
|
|
14
14
|
|
|
15
15
|
export type ContextPart = {
|
|
16
16
|
label: string;
|
|
17
17
|
tokens: number;
|
|
18
|
-
|
|
18
|
+
/** 固定 RGB 色值,不走主题 token——避免不同主题撞色。渲染用 theme.style({fg: color})。 */
|
|
19
|
+
color: Color;
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
type PreviewKey =
|
|
@@ -107,6 +108,7 @@ export async function showTextPreview(
|
|
|
107
108
|
let totalLines = 1;
|
|
108
109
|
let escHovered = false;
|
|
109
110
|
let copyHovered = false;
|
|
111
|
+
let scrollbarHovered = false;
|
|
110
112
|
let escHitbox: { row: number; startCol: number; endCol: number } | undefined;
|
|
111
113
|
let copyHitbox: { row: number; startCol: number; endCol: number } | undefined;
|
|
112
114
|
let scrollbarHitbox:
|
|
@@ -141,6 +143,12 @@ export async function showTextPreview(
|
|
|
141
143
|
tui.requestRender();
|
|
142
144
|
};
|
|
143
145
|
|
|
146
|
+
const setScrollbarHovered = (hovered: boolean): void => {
|
|
147
|
+
if (hovered === scrollbarHovered) return;
|
|
148
|
+
scrollbarHovered = hovered;
|
|
149
|
+
tui.requestRender();
|
|
150
|
+
};
|
|
151
|
+
|
|
144
152
|
const copyPreview = (): void => {
|
|
145
153
|
void copyTextImpl(content)
|
|
146
154
|
.then(() => {
|
|
@@ -168,6 +176,92 @@ export async function showTextPreview(
|
|
|
168
176
|
scrollTo(nextOffset);
|
|
169
177
|
};
|
|
170
178
|
|
|
179
|
+
/**
|
|
180
|
+
* 预览鼠标共用处:1-based 屏幕 col/row + 已解码的按键/动作。
|
|
181
|
+
* handleInput(regular 主屏 raw SGR)与 handleMouse(overlay 事件对象)
|
|
182
|
+
* 都收在这里,两条链同一套 esc/copy/滚动条/滚轮行为。
|
|
183
|
+
* 返回 true 表示这次输入被预览消费。
|
|
184
|
+
*/
|
|
185
|
+
const handlePreviewMouse = (args: {
|
|
186
|
+
col: number;
|
|
187
|
+
row: number;
|
|
188
|
+
release: boolean;
|
|
189
|
+
motion: boolean;
|
|
190
|
+
/** 基础按键码:0 左键、64/65 滚轮上/下。 */
|
|
191
|
+
baseButton: number;
|
|
192
|
+
}): boolean => {
|
|
193
|
+
const { col, row, release, motion, baseButton } = args;
|
|
194
|
+
if (release) {
|
|
195
|
+
scrollbarDragOffset = null;
|
|
196
|
+
setScrollbarHovered(false);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
const overEsc = Boolean(
|
|
200
|
+
escHitbox &&
|
|
201
|
+
row === escHitbox.row &&
|
|
202
|
+
col >= escHitbox.startCol &&
|
|
203
|
+
col <= escHitbox.endCol,
|
|
204
|
+
);
|
|
205
|
+
const overCopy = Boolean(
|
|
206
|
+
copyHitbox &&
|
|
207
|
+
row === copyHitbox.row &&
|
|
208
|
+
col >= copyHitbox.startCol &&
|
|
209
|
+
col <= copyHitbox.endCol,
|
|
210
|
+
);
|
|
211
|
+
setEscHovered(overEsc);
|
|
212
|
+
setCopyHovered(overCopy);
|
|
213
|
+
// 拖动中保持 hover:thumb 一直亮,官方 scrollbarDrag 同理。
|
|
214
|
+
setScrollbarHovered(
|
|
215
|
+
Boolean(
|
|
216
|
+
scrollbarHitbox &&
|
|
217
|
+
col === scrollbarHitbox.col &&
|
|
218
|
+
row >= scrollbarHitbox.startRow &&
|
|
219
|
+
row <= scrollbarHitbox.endRow,
|
|
220
|
+
) || scrollbarDragOffset !== null,
|
|
221
|
+
);
|
|
222
|
+
if (baseButton === 0 && !motion) {
|
|
223
|
+
if (overEsc) {
|
|
224
|
+
done(undefined);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
if (overCopy) {
|
|
228
|
+
copyPreview();
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
if (
|
|
232
|
+
scrollbarHitbox &&
|
|
233
|
+
col === scrollbarHitbox.col &&
|
|
234
|
+
row >= scrollbarHitbox.startRow &&
|
|
235
|
+
row <= scrollbarHitbox.endRow
|
|
236
|
+
) {
|
|
237
|
+
const trackRow = row - scrollbarHitbox.startRow;
|
|
238
|
+
const inThumb =
|
|
239
|
+
trackRow >= scrollbarHitbox.thumbStart &&
|
|
240
|
+
trackRow < scrollbarHitbox.thumbStart + scrollbarHitbox.thumbSize;
|
|
241
|
+
scrollbarDragOffset = inThumb
|
|
242
|
+
? trackRow - scrollbarHitbox.thumbStart
|
|
243
|
+
: Math.floor(scrollbarHitbox.thumbSize / 2);
|
|
244
|
+
dragScrollbarTo(row);
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (motion && scrollbarDragOffset !== null) {
|
|
249
|
+
dragScrollbarTo(row);
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
if (baseButton === 64) {
|
|
253
|
+
scrollTo(scrollOffset - 3);
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
if (baseButton === 65) {
|
|
257
|
+
scrollTo(scrollOffset + 3);
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
// 预览矩形内但未命中任何控件的包也算预览的:返回 true 让 overlay
|
|
261
|
+
// 命中它(hit 成立),官方不再往底下的 transcript 分发。
|
|
262
|
+
return true;
|
|
263
|
+
};
|
|
264
|
+
|
|
171
265
|
return {
|
|
172
266
|
invalidate() {
|
|
173
267
|
markdownView.invalidate();
|
|
@@ -183,62 +277,24 @@ export async function showTextPreview(
|
|
|
183
277
|
else if (matchesKey(data, "pageDown")) scrollTo(scrollOffset + pageSize);
|
|
184
278
|
else if (matchesKey(data, Key.home)) scrollTo(0);
|
|
185
279
|
else if (matchesKey(data, Key.end)) scrollTo(totalLines - pageSize);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
);
|
|
205
|
-
setEscHovered(overEsc);
|
|
206
|
-
setCopyHovered(overCopy);
|
|
207
|
-
const button = mouseBaseButton(mouse.code);
|
|
208
|
-
const motion = (mouse.code & 32) !== 0;
|
|
209
|
-
if (button === 0 && !motion) {
|
|
210
|
-
if (overEsc) {
|
|
211
|
-
done(undefined);
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
if (overCopy) {
|
|
215
|
-
copyPreview();
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
if (
|
|
219
|
-
scrollbarHitbox &&
|
|
220
|
-
mouse.col === scrollbarHitbox.col &&
|
|
221
|
-
mouse.row >= scrollbarHitbox.startRow &&
|
|
222
|
-
mouse.row <= scrollbarHitbox.endRow
|
|
223
|
-
) {
|
|
224
|
-
const trackRow = mouse.row - scrollbarHitbox.startRow;
|
|
225
|
-
const inThumb =
|
|
226
|
-
trackRow >= scrollbarHitbox.thumbStart &&
|
|
227
|
-
trackRow < scrollbarHitbox.thumbStart + scrollbarHitbox.thumbSize;
|
|
228
|
-
scrollbarDragOffset = inThumb
|
|
229
|
-
? trackRow - scrollbarHitbox.thumbStart
|
|
230
|
-
: Math.floor(scrollbarHitbox.thumbSize / 2);
|
|
231
|
-
dragScrollbarTo(mouse.row);
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
if (motion && scrollbarDragOffset !== null) {
|
|
236
|
-
dragScrollbarTo(mouse.row);
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
if (button === 64) scrollTo(scrollOffset - 3);
|
|
240
|
-
else if (button === 65) scrollTo(scrollOffset + 3);
|
|
241
|
-
}
|
|
280
|
+
},
|
|
281
|
+
// overlay 事件对象路径:官方 dispatchMouseToOverlay 把鼠标派到
|
|
282
|
+
// handleMouse,event.x/y 是相对弹框左上角的 0-based 局部坐标,
|
|
283
|
+
// 与 hitbox 同基准,天然对齐官方放置位置。
|
|
284
|
+
handleMouse(event: any) {
|
|
285
|
+
// 一次单击 = press + release + 合成 click。动作已在 press 执行,
|
|
286
|
+
// click 只消费不重复触发(否则 copy/esc 会跑两次)。
|
|
287
|
+
if (event.type === "click") return { handled: true };
|
|
288
|
+
const wheelUp = event.type === "wheel" && Number(event.wheelDelta) < 0;
|
|
289
|
+
const wheelDown = event.type === "wheel" && Number(event.wheelDelta) > 0;
|
|
290
|
+
handlePreviewMouse({
|
|
291
|
+
col: Number(event.x),
|
|
292
|
+
row: Number(event.y),
|
|
293
|
+
release: event.type === "release",
|
|
294
|
+
motion: event.type === "move" || event.type === "drag",
|
|
295
|
+
baseButton: wheelUp ? 64 : wheelDown ? 65 : event.button === "left" ? 0 : -1,
|
|
296
|
+
});
|
|
297
|
+
return { handled: true };
|
|
242
298
|
},
|
|
243
299
|
|
|
244
300
|
render(width: number) {
|
|
@@ -257,16 +313,24 @@ export async function showTextPreview(
|
|
|
257
313
|
const wrapped = markdownView.render(bodyWidth);
|
|
258
314
|
totalLines = wrapped.length;
|
|
259
315
|
scrollOffset = Math.min(scrollOffset, Math.max(0, totalLines - pageSize));
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
316
|
+
// hitbox 用弹框局部坐标(0-based):title 固定第 1 行,esc/copy
|
|
317
|
+
// 在行尾,滚动条字符在 body 行右 border 内侧(width-2 列)。
|
|
318
|
+
escHitbox = { row: 1, startCol: width - 6, endCol: width - 2 };
|
|
319
|
+
copyHitbox = {
|
|
320
|
+
row: 1,
|
|
321
|
+
startCol: width - 6 - 1 - visibleWidth(COPY_LABEL),
|
|
322
|
+
endCol: width - 8,
|
|
323
|
+
};
|
|
265
324
|
const visible = wrapped.slice(scrollOffset, scrollOffset + pageSize);
|
|
266
325
|
const border = (text: string) => theme.fg("border", text);
|
|
267
326
|
const scrollable = totalLines > pageSize;
|
|
327
|
+
// thumb 几何对齐官方 getScrollbarGeometry:trackHeight=pageSize、
|
|
328
|
+
// contentHeight=totalLines,minThumb=min(2,track),thumb=round(track²/content)。
|
|
268
329
|
const thumbSize = scrollable
|
|
269
|
-
? Math.max(
|
|
330
|
+
? Math.max(
|
|
331
|
+
Math.min(2, pageSize),
|
|
332
|
+
Math.min(pageSize, Math.round((pageSize * pageSize) / totalLines)),
|
|
333
|
+
)
|
|
270
334
|
: 0;
|
|
271
335
|
const maxScrollOffset = Math.max(0, totalLines - pageSize);
|
|
272
336
|
const thumbStart =
|
|
@@ -275,9 +339,9 @@ export async function showTextPreview(
|
|
|
275
339
|
: 0;
|
|
276
340
|
scrollbarHitbox = scrollable
|
|
277
341
|
? {
|
|
278
|
-
col:
|
|
279
|
-
startRow:
|
|
280
|
-
endRow:
|
|
342
|
+
col: width - 2,
|
|
343
|
+
startRow: 3,
|
|
344
|
+
endRow: 2 + pageSize,
|
|
281
345
|
thumbStart,
|
|
282
346
|
thumbSize,
|
|
283
347
|
maxOffset: maxScrollOffset,
|
|
@@ -286,7 +350,10 @@ export async function showTextPreview(
|
|
|
286
350
|
const scrollbar = (row: number): string => {
|
|
287
351
|
if (!scrollable) return " ";
|
|
288
352
|
const inThumb = row >= thumbStart && row < thumbStart + thumbSize;
|
|
289
|
-
|
|
353
|
+
// 对齐官方:thumb 常态 ┃、hover/拖动中 █;track 始终 │。
|
|
354
|
+
const thumbActive = scrollbarHovered || scrollbarDragOffset !== null;
|
|
355
|
+
if (inThumb) return theme.fg("accent", thumbActive ? "█" : "┃");
|
|
356
|
+
return theme.fg("borderMuted", "│");
|
|
290
357
|
};
|
|
291
358
|
const bodyRows = Array.from({ length: pageSize }, (_, row) => {
|
|
292
359
|
const line = visible[row] ?? "";
|
|
@@ -294,7 +361,7 @@ export async function showTextPreview(
|
|
|
294
361
|
});
|
|
295
362
|
const start = totalLines === 0 ? 0 : scrollOffset + 1;
|
|
296
363
|
const end = Math.min(totalLines, scrollOffset + pageSize);
|
|
297
|
-
const status = `${start}-${end} / ${totalLines} lines · ↑↓ PgUp/PgDn Home/End
|
|
364
|
+
const status = `${start}-${end} / ${totalLines} lines · ↑↓ PgUp/PgDn Home/End`;
|
|
298
365
|
|
|
299
366
|
return [
|
|
300
367
|
border(`╭${"─".repeat(inner)}╮`),
|
|
@@ -312,7 +379,7 @@ export async function showTextPreview(
|
|
|
312
379
|
overlay: true,
|
|
313
380
|
overlayOptions: {
|
|
314
381
|
anchor: "center",
|
|
315
|
-
width: "
|
|
382
|
+
width: "80%",
|
|
316
383
|
minWidth: 50,
|
|
317
384
|
maxHeight: "80%",
|
|
318
385
|
margin: 2,
|
|
@@ -330,19 +397,15 @@ const tokenEstimate = (value: unknown): number => {
|
|
|
330
397
|
return Math.max(0, Math.ceil(text.length / 4));
|
|
331
398
|
};
|
|
332
399
|
|
|
333
|
-
/**
|
|
400
|
+
/** 只算会进请求正文的 content 和 sections。工具 schema 走 tools 参数,由 Tools definition 单独计一次。 */
|
|
334
401
|
function systemMessageTokens(message: {
|
|
335
402
|
content?: unknown;
|
|
336
403
|
sections?: Record<string, string | null> | null;
|
|
337
|
-
toolsAdded?: unknown;
|
|
338
|
-
toolsRemoved?: unknown;
|
|
339
404
|
}): number {
|
|
340
405
|
let chars = typeof message.content === "string" ? message.content.length : 0;
|
|
341
406
|
for (const section of Object.values(message.sections ?? {})) {
|
|
342
407
|
if (typeof section === "string") chars += section.length;
|
|
343
408
|
}
|
|
344
|
-
if (message.toolsAdded) chars += JSON.stringify(message.toolsAdded).length;
|
|
345
|
-
if (message.toolsRemoved) chars += JSON.stringify(message.toolsRemoved).length;
|
|
346
409
|
return Math.ceil(chars / 4);
|
|
347
410
|
}
|
|
348
411
|
|
|
@@ -527,12 +590,16 @@ export function collectContextBreakdown(
|
|
|
527
590
|
systemTokens = Math.max(0, systemTokens);
|
|
528
591
|
return {
|
|
529
592
|
parts: [
|
|
530
|
-
{ label: "System prompt", tokens: systemTokens, color:
|
|
531
|
-
{ label: "Memory", tokens: memoryTokens, color:
|
|
532
|
-
{ label: "Skills", tokens: skillsTokens, color:
|
|
533
|
-
{
|
|
534
|
-
|
|
535
|
-
|
|
593
|
+
{ label: "System prompt", tokens: systemTokens, color: rgbColor(0x79, 0xc0, 0xff) },
|
|
594
|
+
{ label: "Memory", tokens: memoryTokens, color: rgbColor(0xff, 0x7b, 0x72) },
|
|
595
|
+
{ label: "Skills", tokens: skillsTokens, color: rgbColor(0xcb, 0x92, 0x14) },
|
|
596
|
+
{
|
|
597
|
+
label: "Tools definition",
|
|
598
|
+
tokens: toolDefinitionTokens,
|
|
599
|
+
color: rgbColor(0x37, 0xb3, 0x4a),
|
|
600
|
+
},
|
|
601
|
+
{ label: "Tool results", tokens: toolResultTokens, color: rgbColor(0xf7, 0x78, 0xba) },
|
|
602
|
+
{ label: "Context", tokens: contextTokens, color: rgbColor(0xf0, 0x88, 0x3e) },
|
|
536
603
|
] satisfies ContextPart[],
|
|
537
604
|
previews: {
|
|
538
605
|
systemPrompt:
|
|
@@ -566,8 +633,8 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
|
|
|
566
633
|
const free = Math.max(0, contextWindow - used);
|
|
567
634
|
const allParts = [
|
|
568
635
|
...parts,
|
|
569
|
-
{ label: "Other", tokens: other, color:
|
|
570
|
-
{ label: "Free space", tokens: free, color:
|
|
636
|
+
{ label: "Other", tokens: other, color: rgbColor(0x92, 0x9e, 0xab) },
|
|
637
|
+
{ label: "Free space", tokens: free, color: rgbColor(0x76, 0x84, 0x94) },
|
|
571
638
|
];
|
|
572
639
|
|
|
573
640
|
if (ctx.mode !== "tui") {
|
|
@@ -642,6 +709,59 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
|
|
|
642
709
|
let escHovered = false;
|
|
643
710
|
let hoveredKey: PreviewKey | undefined;
|
|
644
711
|
|
|
712
|
+
/** context 主弹框鼠标共用处:handleInput(raw SGR)与 handleMouse(overlay
|
|
713
|
+
* 事件对象)同一套 hover/选中/关闭行为。返回 true 表示被预览消费。 */
|
|
714
|
+
const handleDialogMouse = (args: {
|
|
715
|
+
col: number;
|
|
716
|
+
row: number;
|
|
717
|
+
release: boolean;
|
|
718
|
+
motion: boolean;
|
|
719
|
+
baseButton: number;
|
|
720
|
+
}): boolean => {
|
|
721
|
+
const { col, row, release, motion, baseButton } = args;
|
|
722
|
+
if (release) return true;
|
|
723
|
+
if (motion) {
|
|
724
|
+
const overEsc = Boolean(
|
|
725
|
+
escHitbox &&
|
|
726
|
+
row === escHitbox.row &&
|
|
727
|
+
col >= escHitbox.startCol &&
|
|
728
|
+
col <= escHitbox.endCol,
|
|
729
|
+
);
|
|
730
|
+
const hovered = previewHitboxes.find(
|
|
731
|
+
(candidate) =>
|
|
732
|
+
row === candidate.row && col >= candidate.startCol && col <= candidate.endCol,
|
|
733
|
+
);
|
|
734
|
+
if (overEsc !== escHovered || hovered?.key !== hoveredKey) {
|
|
735
|
+
escHovered = overEsc;
|
|
736
|
+
hoveredKey = hovered?.key;
|
|
737
|
+
tui.requestRender();
|
|
738
|
+
}
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
741
|
+
if (baseButton !== 0) return true;
|
|
742
|
+
if (
|
|
743
|
+
escHitbox &&
|
|
744
|
+
row === escHitbox.row &&
|
|
745
|
+
col >= escHitbox.startCol &&
|
|
746
|
+
col <= escHitbox.endCol
|
|
747
|
+
) {
|
|
748
|
+
done(undefined);
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
751
|
+
const hitbox = previewHitboxes.find(
|
|
752
|
+
(candidate) =>
|
|
753
|
+
row === candidate.row && col >= candidate.startCol && col <= candidate.endCol,
|
|
754
|
+
);
|
|
755
|
+
if (hitbox) {
|
|
756
|
+
selectedPreviewIndex = Math.max(
|
|
757
|
+
0,
|
|
758
|
+
visiblePreviews.findIndex((preview) => preview.key === hitbox.key),
|
|
759
|
+
);
|
|
760
|
+
done(hitbox.key);
|
|
761
|
+
}
|
|
762
|
+
return true;
|
|
763
|
+
};
|
|
764
|
+
|
|
645
765
|
return {
|
|
646
766
|
invalidate() {},
|
|
647
767
|
handleInput(data: string) {
|
|
@@ -664,53 +784,20 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
|
|
|
664
784
|
done(visiblePreviews[selectedPreviewIndex]?.key);
|
|
665
785
|
return;
|
|
666
786
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
mouse.col >= candidate.startCol &&
|
|
682
|
-
mouse.col <= candidate.endCol,
|
|
683
|
-
);
|
|
684
|
-
if (overEsc !== escHovered || hovered?.key !== hoveredKey) {
|
|
685
|
-
escHovered = overEsc;
|
|
686
|
-
hoveredKey = hovered?.key;
|
|
687
|
-
tui.requestRender();
|
|
688
|
-
}
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
691
|
-
if (mouseBaseButton(mouse.code) !== 0) return;
|
|
692
|
-
if (
|
|
693
|
-
escHitbox &&
|
|
694
|
-
mouse.row === escHitbox.row &&
|
|
695
|
-
mouse.col >= escHitbox.startCol &&
|
|
696
|
-
mouse.col <= escHitbox.endCol
|
|
697
|
-
) {
|
|
698
|
-
done(undefined);
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
const hitbox = previewHitboxes.find(
|
|
702
|
-
(candidate) =>
|
|
703
|
-
mouse.row === candidate.row &&
|
|
704
|
-
mouse.col >= candidate.startCol &&
|
|
705
|
-
mouse.col <= candidate.endCol,
|
|
706
|
-
);
|
|
707
|
-
if (hitbox) {
|
|
708
|
-
selectedPreviewIndex = Math.max(
|
|
709
|
-
0,
|
|
710
|
-
visiblePreviews.findIndex((preview) => preview.key === hitbox.key),
|
|
711
|
-
);
|
|
712
|
-
done(hitbox.key);
|
|
713
|
-
}
|
|
787
|
+
},
|
|
788
|
+
// overlay 事件对象路径:event.x/y 是弹框局部 0-based 坐标,
|
|
789
|
+
// 与 hitbox 同基准。click 是 press+release 的合成,动作已在
|
|
790
|
+
// press 做过,只消费。
|
|
791
|
+
handleMouse(event: any) {
|
|
792
|
+
if (event.type === "click") return { handled: true };
|
|
793
|
+
handleDialogMouse({
|
|
794
|
+
col: Number(event.x),
|
|
795
|
+
row: Number(event.y),
|
|
796
|
+
release: event.type === "release",
|
|
797
|
+
motion: event.type === "move" || event.type === "drag",
|
|
798
|
+
baseButton: event.button === "left" ? 0 : -1,
|
|
799
|
+
});
|
|
800
|
+
return { handled: true };
|
|
714
801
|
},
|
|
715
802
|
render(width: number) {
|
|
716
803
|
const inner = Math.max(1, width - 2);
|
|
@@ -730,7 +817,7 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
|
|
|
730
817
|
Math.round((part.tokens / Math.max(1, contextWindow)) * barWidth),
|
|
731
818
|
);
|
|
732
819
|
remaining -= cells;
|
|
733
|
-
return theme.
|
|
820
|
+
return theme.style("█".repeat(Math.max(0, cells)), { fg: part.color });
|
|
734
821
|
})
|
|
735
822
|
.join("");
|
|
736
823
|
const labelWidth = Math.min(
|
|
@@ -740,7 +827,7 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
|
|
|
740
827
|
const selectedLabel = visiblePreviews[selectedPreviewIndex]?.label;
|
|
741
828
|
const partRows = allParts.map((part) => {
|
|
742
829
|
const pct = contextWindow > 0 ? (part.tokens / contextWindow) * 100 : 0;
|
|
743
|
-
const swatch = theme.fg
|
|
830
|
+
const swatch = theme.style("■", { fg: part.color });
|
|
744
831
|
const label = part.label.padEnd(labelWidth);
|
|
745
832
|
const amount = `${formatTokens(part.tokens).padStart(7)} ${pct.toFixed(1).padStart(5)}%`;
|
|
746
833
|
const selected = part.label === selectedLabel;
|
|
@@ -769,10 +856,10 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
|
|
|
769
856
|
Math.max(1, terminalHeight - 2),
|
|
770
857
|
);
|
|
771
858
|
const visibleHeight = Math.min(lines.length, maxHeight);
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
escHitbox =
|
|
859
|
+
// hitbox 用弹框局部坐标(0-based):esc 在 title 行(第 1 行)
|
|
860
|
+
// 尾部 │ 前 5 列,preview 行对应 partRows(顶 border=0、title=1、
|
|
861
|
+
// ├=2、segments=3、空行=4,part 从 5 起)。不依赖官方放置行列。
|
|
862
|
+
escHitbox = { row: 1, startCol: width - 6, endCol: width - 2 };
|
|
776
863
|
previewHitboxes = visiblePreviews.flatMap((preview) => {
|
|
777
864
|
const partIndex = allParts.findIndex((part) => part.label === preview.label);
|
|
778
865
|
const line = 5 + partIndex;
|
|
@@ -780,9 +867,9 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
|
|
|
780
867
|
? [
|
|
781
868
|
{
|
|
782
869
|
key: preview.key,
|
|
783
|
-
row:
|
|
784
|
-
startCol:
|
|
785
|
-
endCol:
|
|
870
|
+
row: line,
|
|
871
|
+
startCol: 0,
|
|
872
|
+
endCol: width - 1,
|
|
786
873
|
},
|
|
787
874
|
]
|
|
788
875
|
: [];
|
|
@@ -1201,6 +1201,8 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1201
1201
|
};
|
|
1202
1202
|
let activeRound: CompactRound | undefined;
|
|
1203
1203
|
let roundByComponent = new WeakMap<object, CompactRound>();
|
|
1204
|
+
/** 本 pass 建起的回合(含被收编的):清扫孤儿尾行的在册名单。 */
|
|
1205
|
+
const liveRounds = new Set<CompactRound>();
|
|
1204
1206
|
const expandedRoundToolIds = new Set<string>();
|
|
1205
1207
|
/** 用户显式点开的 round 内工具;强制折叠不压回这些 id。 */
|
|
1206
1208
|
const explicitRoundToolIds = new Set<string>();
|
|
@@ -1259,11 +1261,30 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1259
1261
|
pendingFoldRounds.delete(round);
|
|
1260
1262
|
};
|
|
1261
1263
|
|
|
1262
|
-
/**
|
|
1263
|
-
const
|
|
1264
|
+
/** 供采纳为回合边界的 timestamp:必须是看起来像 epoch 毫秒的数(桩数据/秒级会被拒)。 */
|
|
1265
|
+
const isUsableTimestamp = (value: unknown): value is number =>
|
|
1266
|
+
typeof value === "number" &&
|
|
1267
|
+
Number.isFinite(value) &&
|
|
1268
|
+
value >= 1_600_000_000_000 &&
|
|
1269
|
+
value <= Date.now() + 60_000;
|
|
1270
|
+
|
|
1271
|
+
/** 结束回合活动态;不做任何重绘。endTimestamp 用后一段内容的真实时刻收边(回放里
|
|
1272
|
+
* 同一毫秒完成的 create/finalize 差不出真实耗时);缺省取回合内消息的最大 timestamp。 */
|
|
1273
|
+
const finalizeRound = (round: CompactRound, endTimestamp?: number): void => {
|
|
1264
1274
|
clearFoldTimer(round);
|
|
1265
1275
|
round.active = false;
|
|
1266
|
-
if (round.endedAt === undefined)
|
|
1276
|
+
if (round.endedAt === undefined) {
|
|
1277
|
+
let fallback = 0;
|
|
1278
|
+
for (const message of roundMessages(round)) {
|
|
1279
|
+
if (isUsableTimestamp(message?.timestamp) && message.timestamp > fallback) {
|
|
1280
|
+
fallback = message.timestamp;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
round.endedAt = Math.max(
|
|
1284
|
+
round.startedAt,
|
|
1285
|
+
isUsableTimestamp(endTimestamp) ? endTimestamp : fallback || Date.now(),
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1267
1288
|
if (activeRound === round) {
|
|
1268
1289
|
activeRound = undefined;
|
|
1269
1290
|
deps.query?.setCompactSummaryActive?.(false);
|
|
@@ -1275,13 +1296,13 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1275
1296
|
* 结束回合活动态;render=true 时走静默期,延迟 FOLD_SETTLE_MS 再收拢。
|
|
1276
1297
|
* 分组语义不受影响:activeRound 立即让位,新消息不会被并进已收尾的回合。
|
|
1277
1298
|
*/
|
|
1278
|
-
const endRound = (round: CompactRound, render = false): void => {
|
|
1299
|
+
const endRound = (round: CompactRound, render = false, endTimestamp?: number): void => {
|
|
1279
1300
|
if (!render || !round.active) {
|
|
1280
|
-
finalizeRound(round);
|
|
1301
|
+
finalizeRound(round, endTimestamp);
|
|
1281
1302
|
if (render) renderRound(round);
|
|
1282
1303
|
return;
|
|
1283
1304
|
}
|
|
1284
|
-
finalizeRound(round);
|
|
1305
|
+
finalizeRound(round, endTimestamp);
|
|
1285
1306
|
if (round.foldTimer !== undefined) return;
|
|
1286
1307
|
round.settleUntil = Date.now() + FOLD_SETTLE_MS;
|
|
1287
1308
|
pendingFoldRounds.add(round);
|
|
@@ -1462,7 +1483,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1462
1483
|
/** 取并入目标;旧回合被用户消息隔开时先收尾(静默期一转场,新回合立即接手)。 */
|
|
1463
1484
|
const joinOrCreateRound = (component: any, message: any): CompactRound => {
|
|
1464
1485
|
const joinable = joinableActiveRound(component);
|
|
1465
|
-
if (!joinable && activeRound) finishRound(activeRound);
|
|
1486
|
+
if (!joinable && activeRound) finishRound(activeRound, message?.timestamp);
|
|
1466
1487
|
return joinable ?? createRound(component, message);
|
|
1467
1488
|
};
|
|
1468
1489
|
|
|
@@ -1531,10 +1552,13 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1531
1552
|
};
|
|
1532
1553
|
|
|
1533
1554
|
/**
|
|
1534
|
-
*
|
|
1535
|
-
* transcript
|
|
1536
|
-
*
|
|
1537
|
-
*
|
|
1555
|
+
* 尾行归位。目标位:活回合(运行中/收尾静默期)且未展开 → 容器最末,
|
|
1556
|
+
* 计数行钉在 transcript 可写底缘,运行中到达的提示行/custom 条目只能落在
|
|
1557
|
+
* 它上方,不会被顶离底部截成两行;其余(收尾回合、展开的回合)→ 回合最后
|
|
1558
|
+
* 一个成员之后,外来行统一换到摘要行之下。展开的回合不按 live 推到底:
|
|
1559
|
+
* 展开卡属于回合原位,推进容器末尾会把它挪到别的消息之间。
|
|
1560
|
+
* 尾行已在目标位就不动容器:展开/收起只换 tail.parts,无谓的重摘重插
|
|
1561
|
+
* 本身就是摘要行挪位的来源。
|
|
1538
1562
|
*/
|
|
1539
1563
|
const syncTailPosition = (round: CompactRound): void => {
|
|
1540
1564
|
const tail = round.tail;
|
|
@@ -1546,11 +1570,20 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1546
1570
|
if (!container || !Array.isArray(container.children)) return;
|
|
1547
1571
|
tail.container = container;
|
|
1548
1572
|
const children = container.children as any[];
|
|
1573
|
+
const isMember = roundMemberMatcher(round);
|
|
1549
1574
|
const current = children.indexOf(tail.host);
|
|
1575
|
+
// 摘尾行前先定目标位,已在位就整段跳过。
|
|
1576
|
+
let last = -1;
|
|
1577
|
+
for (let i = 0; i < children.length; i++) {
|
|
1578
|
+
if (children[i] === tail.host || !isMember(children[i])) continue;
|
|
1579
|
+
last = i;
|
|
1580
|
+
}
|
|
1581
|
+
const pinToEnd = roundLive(round) && round.anchor?.expanded !== true;
|
|
1582
|
+
const desired = pinToEnd || last < 0 ? children.length - 1 : last + 1;
|
|
1583
|
+
if (current === desired) return;
|
|
1550
1584
|
if (current >= 0) children.splice(current, 1);
|
|
1551
|
-
const isMember = roundMemberMatcher(round);
|
|
1552
1585
|
let first = -1;
|
|
1553
|
-
|
|
1586
|
+
last = -1;
|
|
1554
1587
|
for (let i = 0; i < children.length; i++) {
|
|
1555
1588
|
if (!isMember(children[i])) continue;
|
|
1556
1589
|
if (first < 0) first = i;
|
|
@@ -1565,7 +1598,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1565
1598
|
}
|
|
1566
1599
|
if (lastMember) last = children.indexOf(lastMember);
|
|
1567
1600
|
const insertAt = last < 0 ? children.length : last + 1;
|
|
1568
|
-
if (
|
|
1601
|
+
if (pinToEnd) {
|
|
1569
1602
|
// 活回合:外来行压到最后成员之后,尾行占容器最末。
|
|
1570
1603
|
children.splice(insertAt, 0, ...intruders);
|
|
1571
1604
|
children.push(tail.host);
|
|
@@ -1640,7 +1673,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1640
1673
|
}
|
|
1641
1674
|
};
|
|
1642
1675
|
|
|
1643
|
-
/**
|
|
1676
|
+
/** 新建活回合:起点用消息真实时刻(回放里 Date.now 会把整轮挤在同一毫秒)。 */
|
|
1644
1677
|
const createRound = (anchor: any, message: any): CompactRound => {
|
|
1645
1678
|
const round: CompactRound = {
|
|
1646
1679
|
anchor,
|
|
@@ -1649,8 +1682,9 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
1649
1682
|
active: true,
|
|
1650
1683
|
suppressedToolIds: new Set(),
|
|
1651
1684
|
liveSlotToolIds: new Set(),
|
|
1652
|
-
startedAt: Date.now(),
|
|
1685
|
+
startedAt: isUsableTimestamp(message?.timestamp) ? message.timestamp : Date.now(),
|
|
1653
1686
|
};
|
|
1687
|
+
liveRounds.add(round);
|
|
1654
1688
|
activateRound(round);
|
|
1655
1689
|
adoptPriorRound(round, message);
|
|
1656
1690
|
return round;
|
|
@@ -2014,11 +2048,32 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2014
2048
|
ensureRoundTick();
|
|
2015
2049
|
};
|
|
2016
2050
|
|
|
2017
|
-
|
|
2051
|
+
/** 结束回合活动态;render=true 时走静默期,延迟 FOLD_SETTLE_MS 再收拢。 */
|
|
2052
|
+
const finishRound = (round: CompactRound, endTimestamp?: number): void =>
|
|
2053
|
+
endRound(round, true, endTimestamp);
|
|
2054
|
+
|
|
2055
|
+
/**
|
|
2056
|
+
* 重放/重建结束后的对账:mountedTails 里凡 tail.round 不在本 pass 回合名单的
|
|
2057
|
+
* 都是没被收编的孤儿(anchor 组件没了、消息换成了新对象、回放顺序错位),
|
|
2058
|
+
* 就地摘除。收编过的尾行 round 已过户到新 round,不在此列。
|
|
2059
|
+
*/
|
|
2060
|
+
const sweepOrphanTails = (): void => {
|
|
2061
|
+
for (const tail of [...mountedTails]) {
|
|
2062
|
+
const round = tail.round as CompactRound | undefined;
|
|
2063
|
+
if (round && liveRounds.has(round)) continue;
|
|
2064
|
+
removeTailHost(tail);
|
|
2065
|
+
mountedTails.delete(tail);
|
|
2066
|
+
if (round?.anchor && tailByAnchor.get(round.anchor) === tail) {
|
|
2067
|
+
tailByAnchor.delete(round.anchor);
|
|
2068
|
+
}
|
|
2069
|
+
if (round?.tail === tail) round.tail = undefined;
|
|
2070
|
+
}
|
|
2071
|
+
};
|
|
2018
2072
|
|
|
2019
2073
|
const resetRounds = (): void => {
|
|
2020
2074
|
for (const round of [...pendingFoldRounds]) clearFoldTimer(round);
|
|
2021
2075
|
activeRound = undefined;
|
|
2076
|
+
liveRounds.clear();
|
|
2022
2077
|
roundByComponent = new WeakMap();
|
|
2023
2078
|
expandedRoundToolIds.clear();
|
|
2024
2079
|
explicitRoundToolIds.clear();
|
|
@@ -2088,7 +2143,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2088
2143
|
round.messages.delete(this);
|
|
2089
2144
|
clearRoundAnchor(this);
|
|
2090
2145
|
}
|
|
2091
|
-
if (activeRound) finishRound(activeRound);
|
|
2146
|
+
if (activeRound) finishRound(activeRound, message.timestamp);
|
|
2092
2147
|
round = createRound(this, message);
|
|
2093
2148
|
roundByComponent.set(this, round);
|
|
2094
2149
|
} else if (!round) {
|
|
@@ -2097,6 +2152,9 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2097
2152
|
roundByComponent.set(this, round);
|
|
2098
2153
|
}
|
|
2099
2154
|
round.messages.set(this, message);
|
|
2155
|
+
if (isUsableTimestamp(message.timestamp) && message.timestamp < round.startedAt) {
|
|
2156
|
+
round.startedAt = message.timestamp;
|
|
2157
|
+
}
|
|
2100
2158
|
renderRound(round);
|
|
2101
2159
|
return undefined;
|
|
2102
2160
|
}
|
|
@@ -2111,17 +2169,17 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2111
2169
|
clearRoundAnchor(this);
|
|
2112
2170
|
round.detachedMessages.push(previousMessage);
|
|
2113
2171
|
roundByComponent.delete(this);
|
|
2114
|
-
finishRound(round);
|
|
2172
|
+
finishRound(round, message.timestamp);
|
|
2115
2173
|
return renderAssistantWithoutThinking(this, message);
|
|
2116
2174
|
}
|
|
2117
2175
|
if (round) {
|
|
2118
2176
|
// 围观态下 anchor 还挂着槽位卡,必须重绘才能收回。
|
|
2119
|
-
endRound(round, true);
|
|
2177
|
+
endRound(round, true, message.timestamp);
|
|
2120
2178
|
roundByComponent.delete(this);
|
|
2121
2179
|
clearRoundAnchor(this);
|
|
2122
2180
|
return passThroughAssistant(this, message);
|
|
2123
2181
|
}
|
|
2124
|
-
if (activeRound) finishRound(activeRound);
|
|
2182
|
+
if (activeRound) finishRound(activeRound, message.timestamp);
|
|
2125
2183
|
return renderAssistantWithoutThinking(this, message);
|
|
2126
2184
|
}
|
|
2127
2185
|
|
|
@@ -2133,6 +2191,9 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2133
2191
|
roundByComponent.set(this, round);
|
|
2134
2192
|
}
|
|
2135
2193
|
round.messages.set(this, message);
|
|
2194
|
+
if (isUsableTimestamp(message.timestamp) && message.timestamp < round.startedAt) {
|
|
2195
|
+
round.startedAt = message.timestamp;
|
|
2196
|
+
}
|
|
2136
2197
|
renderRound(round);
|
|
2137
2198
|
return undefined;
|
|
2138
2199
|
}
|
|
@@ -2140,7 +2201,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2140
2201
|
// 无可见内容的 abort/error/length:直接外层状态行,并结束进行中的回合。
|
|
2141
2202
|
const loneStatus = messageStopStatus(message);
|
|
2142
2203
|
if (loneStatus) {
|
|
2143
|
-
if (activeRound) finishRound(activeRound);
|
|
2204
|
+
if (activeRound) finishRound(activeRound, message.timestamp);
|
|
2144
2205
|
self.contentContainer.clear();
|
|
2145
2206
|
appendStopStatus(this, loneStatus);
|
|
2146
2207
|
return undefined;
|
|
@@ -2265,6 +2326,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2265
2326
|
if (config.mode === "compact") syncGlobalExpanded(ctx);
|
|
2266
2327
|
else hoveredAssistantComponent = undefined;
|
|
2267
2328
|
refreshTrackedComponents();
|
|
2329
|
+
sweepOrphanTails();
|
|
2268
2330
|
},
|
|
2269
2331
|
refresh() {
|
|
2270
2332
|
if (!patch.active) return;
|
|
@@ -2272,6 +2334,7 @@ export function installCompactMode(deps: CompactModeInstallDeps): CompactModeHoo
|
|
|
2272
2334
|
patch.assertAssistantOwnership();
|
|
2273
2335
|
if (config.mode !== "compact") hoveredAssistantComponent = undefined;
|
|
2274
2336
|
refreshTrackedComponents();
|
|
2337
|
+
sweepOrphanTails();
|
|
2275
2338
|
},
|
|
2276
2339
|
assertOwnership() {
|
|
2277
2340
|
if (!patch.active) return;
|
|
@@ -191,7 +191,7 @@ const FULLSCREEN_FOCUS_OUT = "\x1b[O";
|
|
|
191
191
|
|
|
192
192
|
let pendingCollapsePress: {
|
|
193
193
|
card: any;
|
|
194
|
-
/**
|
|
194
|
+
/** 命中的最内层组件:卡内工具行命中时用于触发官方 click 抑制窗口。 */
|
|
195
195
|
component: any;
|
|
196
196
|
col: number;
|
|
197
197
|
row: number;
|
|
@@ -240,22 +240,17 @@ function rememberCollapsePress(card: any, component: any, packet: SgrMousePacket
|
|
|
240
240
|
|
|
241
241
|
/**
|
|
242
242
|
* 松手结算:只有"按下与松开在同一格"的完整单击才收起,拖动选择文本时保持展开。
|
|
243
|
-
*
|
|
244
|
-
*
|
|
243
|
+
* 官方卡与自有卡统一在这里结算:fullscreen 下结算成功的 release 被 consume,
|
|
244
|
+
* 官方不会再合成 click(合成 click 要求按下/松开坐标完全一致,手抖一格就不发,
|
|
245
|
+
* 还会把刚收起的卡 toggle 回去、或误触卡内其他 MouseRegion)。
|
|
245
246
|
*/
|
|
246
|
-
function resolveCollapsePress(
|
|
247
|
-
tui: any,
|
|
248
|
-
packet: SgrMousePacket,
|
|
249
|
-
options: { skipOfficialCards: boolean },
|
|
250
|
-
): any {
|
|
247
|
+
function resolveCollapsePress(tui: any, packet: SgrMousePacket): any {
|
|
251
248
|
const press = pendingCollapsePress;
|
|
252
249
|
pendingCollapsePress = null;
|
|
253
250
|
if (!press || packet.final !== "m") return undefined;
|
|
254
251
|
if (Math.abs(packet.col - press.col) > COLLAPSE_CLICK_MOVE_TOLERANCE) return undefined;
|
|
255
252
|
if (Math.abs(packet.row - press.row) > COLLAPSE_CLICK_MOVE_TOLERANCE) return undefined;
|
|
256
253
|
if (Date.now() - press.at > COLLAPSE_CLICK_MAX_MS) return undefined;
|
|
257
|
-
if (options.skipOfficialCards && press.component instanceof ToolExecutionComponent)
|
|
258
|
-
return undefined;
|
|
259
254
|
const card = press.card;
|
|
260
255
|
if (
|
|
261
256
|
(card instanceof ToolGroupComponent || isCompactAssistantComponent(card)) &&
|
|
@@ -265,7 +260,7 @@ function resolveCollapsePress(
|
|
|
265
260
|
suppressOfficialCardClickUntil = Date.now() + SUPPRESS_OFFICIAL_CARD_CLICK_MS;
|
|
266
261
|
}
|
|
267
262
|
collapseExpandedCard(tui, card);
|
|
268
|
-
//
|
|
263
|
+
// 返回收起的卡:fullscreen 调用方据此吞掉这次 release 并清官方按下/选区状态。
|
|
269
264
|
return card;
|
|
270
265
|
}
|
|
271
266
|
|
|
@@ -398,7 +393,7 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
398
393
|
if (!target) return false;
|
|
399
394
|
const component = target.component;
|
|
400
395
|
// compact 展开面板内命中时 owner 是面板:收起对象与记账对象都归面板,
|
|
401
|
-
//
|
|
396
|
+
// 面板内工具行命中时 release 统一由扩展结算。
|
|
402
397
|
const card = target.group ?? target.owner ?? component;
|
|
403
398
|
const pressComponent = target.owner ?? component;
|
|
404
399
|
// 回到底部按钮:按组件引用命中,不依赖渲染行缓存。
|
|
@@ -474,8 +469,8 @@ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
474
469
|
});
|
|
475
470
|
}
|
|
476
471
|
}
|
|
477
|
-
// 其余放行官方:选区、OSC8
|
|
478
|
-
//
|
|
472
|
+
// 其余放行官方:选区、OSC8 链接与官方按下登记都交给 renderer。
|
|
473
|
+
// 完整单击的收起统一在松手结算(release 被 consume,官方不会再合成 click)。
|
|
479
474
|
rememberCollapsePress(card, pressComponent, packet);
|
|
480
475
|
return false;
|
|
481
476
|
}
|
|
@@ -584,6 +579,27 @@ function handleFullscreenToolHover(tui: any, packet: SgrMousePacket): void {
|
|
|
584
579
|
applyFullscreenHover(tui, target);
|
|
585
580
|
}
|
|
586
581
|
|
|
582
|
+
/**
|
|
583
|
+
* 鼠标包(1-based 屏幕坐标)是否落在某个已渲染 overlay 的矩形内。
|
|
584
|
+
* renderedOverlayLayouts 与官方 dispatchMouseToOverlay 同源:只含上一帧画出的可见 overlay。
|
|
585
|
+
*/
|
|
586
|
+
function packetInsideOverlay(tui: any, packet: SgrMousePacket): boolean {
|
|
587
|
+
const layouts = tui?.renderedOverlayLayouts;
|
|
588
|
+
if (!Array.isArray(layouts)) return false;
|
|
589
|
+
const x = packet.col - 1;
|
|
590
|
+
const y = packet.row - 1;
|
|
591
|
+
for (const layout of layouts) {
|
|
592
|
+
if (
|
|
593
|
+
x >= layout.col &&
|
|
594
|
+
x < layout.col + layout.width &&
|
|
595
|
+
y >= layout.row &&
|
|
596
|
+
y < layout.row + layout.height
|
|
597
|
+
)
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
|
|
587
603
|
/**
|
|
588
604
|
* 实例级包装 TuiAltScreen.handleViewportInput(惰性 Proxy 安全):
|
|
589
605
|
* 原型方法取 original(绕开 proxy 函数包装),实例 own property 装 wrapper
|
|
@@ -602,24 +618,32 @@ function patchFullscreenViewportInput(tui: any): void {
|
|
|
602
618
|
// 官方 viewport 会消费键盘,扩展监听器无法补偿,必须在这里调度。
|
|
603
619
|
scheduleScrollButtonSync(tui, data);
|
|
604
620
|
const packets = parseSgrMousePackets(data);
|
|
605
|
-
//
|
|
606
|
-
//
|
|
607
|
-
|
|
608
|
-
|
|
621
|
+
// overlay(文本预览/context 弹框,都是真 overlay)可见时:落在
|
|
622
|
+
// overlay 矩形内的包经 packetInsideOverlay 放行官方
|
|
623
|
+
// dispatchMouseToOverlay → overlay.handleMouse;overlay 外的包仍
|
|
624
|
+
// 走扩展收起/展开/hover。两条路径各管各的。
|
|
625
|
+
if (packets) {
|
|
626
|
+
// overlay 可见时:落在 overlay 矩形内的包全部放行官方(文本预览的
|
|
627
|
+
// [esc] 点击/滚轮、官方搜索框等),扩展不展开/收起/hover。
|
|
628
|
+
const overlayPackets = tui.hasOverlay?.()
|
|
629
|
+
? packets.filter((packet) => packetInsideOverlay(tui, packet))
|
|
630
|
+
: undefined;
|
|
631
|
+
const insideOverlay = (packet: SgrMousePacket) => overlayPackets?.includes(packet) === true;
|
|
609
632
|
for (const packet of packets) {
|
|
633
|
+
if (insideOverlay(packet)) continue;
|
|
610
634
|
if (isSgrLeftRelease(packet)) {
|
|
611
|
-
//
|
|
612
|
-
//
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
) {
|
|
635
|
+
// 结算单击收起:成功时吞掉 release,官方不会再对同一次点击合成
|
|
636
|
+
// click(官方 click 要求按下/松开坐标严格一致,还会把刚收起的卡
|
|
637
|
+
// toggle 回去,或误触卡内其他 MouseRegion 区)。
|
|
638
|
+
if (resolveCollapsePress(tui, packet) !== undefined) {
|
|
639
|
+
// 收起会重排布局:官方还留着按下时登记的 press 目标与选区锚点,
|
|
640
|
+
// 发 FOCUS_OUT 一次清干净,避免旧坐标结算出误选/误发 click。
|
|
618
641
|
try {
|
|
619
642
|
Reflect.apply(original, this, [FULLSCREEN_FOCUS_OUT]);
|
|
620
643
|
} catch {
|
|
621
644
|
/* 老版本没有选区状态 */
|
|
622
645
|
}
|
|
646
|
+
return { consume: true };
|
|
623
647
|
}
|
|
624
648
|
} else if (!isSgrLeftPress(packet) && !isSgrIdleMotion(packet))
|
|
625
649
|
clearPendingCollapsePressOnMove(packet);
|
|
@@ -965,12 +989,15 @@ function handleToolMouseInput(data: string): { consume: true } | undefined {
|
|
|
965
989
|
scheduleScrollButtonSync(getToolMouseTui(), data);
|
|
966
990
|
return undefined;
|
|
967
991
|
}
|
|
992
|
+
// 文本预览活动时,点击交给预览自己的 handleInput(esc/copy/滚动条),
|
|
993
|
+
// 不被残留的工具 region 吞掉;预览关闭后恢复。
|
|
994
|
+
if (hasActiveTextPreview()) return undefined;
|
|
968
995
|
|
|
969
996
|
let consumed = false;
|
|
970
997
|
for (const packet of packets) {
|
|
971
998
|
updateToolSummaryHover(getToolMouseTui(), packet);
|
|
972
999
|
if (isSgrLeftRelease(packet)) {
|
|
973
|
-
resolveCollapsePress(getToolMouseTui(), packet
|
|
1000
|
+
resolveCollapsePress(getToolMouseTui(), packet);
|
|
974
1001
|
continue;
|
|
975
1002
|
}
|
|
976
1003
|
if (!isSgrLeftPress(packet)) {
|