pi-cc-extensions 1.0.3 → 1.0.5

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,15 @@ 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 { mouseBaseButton, parseSgrMousePacket } from "../utils/sgr-mouse.ts";
12
+ import { type Color, rgbColor } from "@earendil-works/pi-tui";
13
13
  import { padLine } from "../utils/format.ts";
14
+ import { mouseBaseButton, parseSgrMousePacket } from "../utils/sgr-mouse.ts";
14
15
 
15
16
  export type ContextPart = {
16
17
  label: string;
17
18
  tokens: number;
18
- color: "accent" | "success" | "warning" | "customMessageLabel" | "muted" | "dim" | "error";
19
+ /** 固定 RGB 色值,不走主题 token——避免不同主题撞色。渲染用 theme.style({fg: color})。 */
20
+ color: Color;
19
21
  };
20
22
 
21
23
  type PreviewKey =
@@ -33,6 +35,61 @@ type ContextPreview = {
33
35
  content: string;
34
36
  };
35
37
 
38
+ /** Pi 居中 overlay 的屏幕起点。没有 renderedOverlayLayouts 时用它,避免把 1-based 屏幕坐标当局部坐标。 */
39
+ function centeredOverlayOrigin(
40
+ tui: any,
41
+ margin: number,
42
+ maxHeightPercent: number,
43
+ width: number,
44
+ height: number,
45
+ ) {
46
+ const termWidth = Math.max(1, tui.terminal?.columns ?? 80);
47
+ const termHeight = Math.max(1, tui.terminal?.rows ?? 24);
48
+ const availWidth = Math.max(1, termWidth - margin * 2);
49
+ const availHeight = Math.max(1, termHeight - margin * 2);
50
+ const maxHeight = Math.max(
51
+ 1,
52
+ Math.min(Math.floor((termHeight * maxHeightPercent) / 100), availHeight),
53
+ );
54
+ const effectiveHeight = Math.min(height, maxHeight);
55
+ return {
56
+ col: Math.max(
57
+ margin,
58
+ Math.min(margin + Math.floor((availWidth - width) / 2), termWidth - margin - width),
59
+ ),
60
+ row: Math.max(
61
+ margin,
62
+ Math.min(
63
+ margin + Math.floor((availHeight - effectiveHeight) / 2),
64
+ termHeight - margin - effectiveHeight,
65
+ ),
66
+ ),
67
+ };
68
+ }
69
+
70
+ function localMouseFromSgr(
71
+ tui: any,
72
+ mouse: { code: number; col: number; row: number; final: "M" | "m" },
73
+ fallback: { col: number; row: number },
74
+ ) {
75
+ // 用焦点组件自己的布局。取最上层会在多个 overlay 叠放时把下层的点击算偏。
76
+ const layouts = Array.isArray(tui.renderedOverlayLayouts) ? tui.renderedOverlayLayouts : [];
77
+ const focused = tui.focusedComponent;
78
+ const layout =
79
+ layouts.findLast((candidate: { entry?: { component?: unknown } }) =>
80
+ focused ? candidate.entry?.component === focused : false,
81
+ ) ??
82
+ (focused ? undefined : layouts.at(-1)) ??
83
+ fallback;
84
+ return {
85
+ col: mouse.col - 1 - layout.col,
86
+ row: mouse.row - 1 - layout.row,
87
+ release: mouse.final === "m",
88
+ motion: (mouse.code & 32) !== 0,
89
+ baseButton: mouseBaseButton(mouse.code),
90
+ };
91
+ }
92
+
36
93
  function normalizePreviewText(text: string): string {
37
94
  return text
38
95
  .replace(/\r\n?/g, "\n")
@@ -107,6 +164,7 @@ export async function showTextPreview(
107
164
  let totalLines = 1;
108
165
  let escHovered = false;
109
166
  let copyHovered = false;
167
+ let scrollbarHovered = false;
110
168
  let escHitbox: { row: number; startCol: number; endCol: number } | undefined;
111
169
  let copyHitbox: { row: number; startCol: number; endCol: number } | undefined;
112
170
  let scrollbarHitbox:
@@ -120,6 +178,7 @@ export async function showTextPreview(
120
178
  }
121
179
  | undefined;
122
180
  let scrollbarDragOffset: number | null = null;
181
+ let screenOrigin = { col: 0, row: 0 };
123
182
  const markdownView = new Markdown(content, 0, 0, getMarkdownTheme());
124
183
 
125
184
  const scrollTo = (nextOffset: number): void => {
@@ -141,6 +200,12 @@ export async function showTextPreview(
141
200
  tui.requestRender();
142
201
  };
143
202
 
203
+ const setScrollbarHovered = (hovered: boolean): void => {
204
+ if (hovered === scrollbarHovered) return;
205
+ scrollbarHovered = hovered;
206
+ tui.requestRender();
207
+ };
208
+
144
209
  const copyPreview = (): void => {
145
210
  void copyTextImpl(content)
146
211
  .then(() => {
@@ -168,6 +233,92 @@ export async function showTextPreview(
168
233
  scrollTo(nextOffset);
169
234
  };
170
235
 
236
+ /**
237
+ * 预览鼠标共用处:1-based 屏幕 col/row + 已解码的按键/动作。
238
+ * handleInput(regular 主屏 raw SGR)与 handleMouse(overlay 事件对象)
239
+ * 都收在这里,两条链同一套 esc/copy/滚动条/滚轮行为。
240
+ * 返回 true 表示这次输入被预览消费。
241
+ */
242
+ const handlePreviewMouse = (args: {
243
+ col: number;
244
+ row: number;
245
+ release: boolean;
246
+ motion: boolean;
247
+ /** 基础按键码:0 左键、64/65 滚轮上/下。 */
248
+ baseButton: number;
249
+ }): boolean => {
250
+ const { col, row, release, motion, baseButton } = args;
251
+ if (release) {
252
+ scrollbarDragOffset = null;
253
+ setScrollbarHovered(false);
254
+ return true;
255
+ }
256
+ const overEsc = Boolean(
257
+ escHitbox &&
258
+ row === escHitbox.row &&
259
+ col >= escHitbox.startCol &&
260
+ col <= escHitbox.endCol,
261
+ );
262
+ const overCopy = Boolean(
263
+ copyHitbox &&
264
+ row === copyHitbox.row &&
265
+ col >= copyHitbox.startCol &&
266
+ col <= copyHitbox.endCol,
267
+ );
268
+ setEscHovered(overEsc);
269
+ setCopyHovered(overCopy);
270
+ // 拖动中保持 hover:thumb 一直亮,官方 scrollbarDrag 同理。
271
+ setScrollbarHovered(
272
+ Boolean(
273
+ scrollbarHitbox &&
274
+ col === scrollbarHitbox.col &&
275
+ row >= scrollbarHitbox.startRow &&
276
+ row <= scrollbarHitbox.endRow,
277
+ ) || scrollbarDragOffset !== null,
278
+ );
279
+ if (baseButton === 0 && !motion) {
280
+ if (overEsc) {
281
+ done(undefined);
282
+ return true;
283
+ }
284
+ if (overCopy) {
285
+ copyPreview();
286
+ return true;
287
+ }
288
+ if (
289
+ scrollbarHitbox &&
290
+ col === scrollbarHitbox.col &&
291
+ row >= scrollbarHitbox.startRow &&
292
+ row <= scrollbarHitbox.endRow
293
+ ) {
294
+ const trackRow = row - scrollbarHitbox.startRow;
295
+ const inThumb =
296
+ trackRow >= scrollbarHitbox.thumbStart &&
297
+ trackRow < scrollbarHitbox.thumbStart + scrollbarHitbox.thumbSize;
298
+ scrollbarDragOffset = inThumb
299
+ ? trackRow - scrollbarHitbox.thumbStart
300
+ : Math.floor(scrollbarHitbox.thumbSize / 2);
301
+ dragScrollbarTo(row);
302
+ return true;
303
+ }
304
+ }
305
+ if (motion && scrollbarDragOffset !== null) {
306
+ dragScrollbarTo(row);
307
+ return true;
308
+ }
309
+ if (baseButton === 64) {
310
+ scrollTo(scrollOffset - 3);
311
+ return true;
312
+ }
313
+ if (baseButton === 65) {
314
+ scrollTo(scrollOffset + 3);
315
+ return true;
316
+ }
317
+ // 预览矩形内但未命中任何控件的包也算预览的:返回 true 让 overlay
318
+ // 命中它(hit 成立),官方不再往底下的 transcript 分发。
319
+ return true;
320
+ };
321
+
171
322
  return {
172
323
  invalidate() {
173
324
  markdownView.invalidate();
@@ -186,60 +337,27 @@ export async function showTextPreview(
186
337
  else {
187
338
  const mouse = parseSgrMousePacket(data);
188
339
  if (!mouse) return;
189
- if (mouse.final === "m") {
190
- scrollbarDragOffset = null;
191
- return;
192
- }
193
- const overEsc = Boolean(
194
- escHitbox &&
195
- mouse.row === escHitbox.row &&
196
- mouse.col >= escHitbox.startCol &&
197
- mouse.col <= escHitbox.endCol,
198
- );
199
- const overCopy = Boolean(
200
- copyHitbox &&
201
- mouse.row === copyHitbox.row &&
202
- mouse.col >= copyHitbox.startCol &&
203
- mouse.col <= copyHitbox.endCol,
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);
340
+ handlePreviewMouse(localMouseFromSgr(tui, mouse, screenOrigin));
241
341
  }
242
342
  },
343
+ // overlay 事件对象路径:官方 dispatchMouseToOverlay 把鼠标派到
344
+ // handleMouse,event.x/y 是相对弹框左上角的 0-based 局部坐标,
345
+ // 与 hitbox 同基准,天然对齐官方放置位置。
346
+ handleMouse(event: any) {
347
+ // 一次单击 = press + release + 合成 click。动作已在 press 执行,
348
+ // click 只消费不重复触发(否则 copy/esc 会跑两次)。
349
+ if (event.type === "click") return { handled: true };
350
+ const wheelUp = event.type === "wheel" && Number(event.wheelDelta) < 0;
351
+ const wheelDown = event.type === "wheel" && Number(event.wheelDelta) > 0;
352
+ handlePreviewMouse({
353
+ col: Number(event.x),
354
+ row: Number(event.y),
355
+ release: event.type === "release",
356
+ motion: event.type === "move" || event.type === "drag",
357
+ baseButton: wheelUp ? 64 : wheelDown ? 65 : event.button === "left" ? 0 : -1,
358
+ });
359
+ return { handled: true };
360
+ },
243
361
 
244
362
  render(width: number) {
245
363
  const inner = Math.max(1, width - 2);
@@ -257,16 +375,24 @@ export async function showTextPreview(
257
375
  const wrapped = markdownView.render(bodyWidth);
258
376
  totalLines = wrapped.length;
259
377
  scrollOffset = Math.min(scrollOffset, Math.max(0, totalLines - pageSize));
260
- // Centered overlay with margin 2: mirror TUI resolveOverlayLayout for anchor "center".
261
- const overlayTop = 2 + Math.floor((availableHeight - viewportHeight) / 2);
262
- const overlayLeft = Math.floor((Math.max(1, tui.terminal.columns) - width) / 2);
263
- escHitbox = escCloseHitbox({ left: overlayLeft, top: overlayTop, width });
264
- copyHitbox = copyButtonHitbox({ left: overlayLeft, top: overlayTop, width });
378
+ // hitbox 用弹框局部坐标(0-based):title 固定第 1 行,esc/copy
379
+ // 在行尾,滚动条字符在 body 行右 border 内侧(width-2 列)。
380
+ escHitbox = { row: 1, startCol: width - 6, endCol: width - 2 };
381
+ copyHitbox = {
382
+ row: 1,
383
+ startCol: width - 6 - 1 - visibleWidth(COPY_LABEL),
384
+ endCol: width - 8,
385
+ };
265
386
  const visible = wrapped.slice(scrollOffset, scrollOffset + pageSize);
266
387
  const border = (text: string) => theme.fg("border", text);
267
388
  const scrollable = totalLines > pageSize;
389
+ // thumb 几何对齐官方 getScrollbarGeometry:trackHeight=pageSize、
390
+ // contentHeight=totalLines,minThumb=min(2,track),thumb=round(track²/content)。
268
391
  const thumbSize = scrollable
269
- ? Math.max(1, Math.floor((pageSize * pageSize) / totalLines))
392
+ ? Math.max(
393
+ Math.min(2, pageSize),
394
+ Math.min(pageSize, Math.round((pageSize * pageSize) / totalLines)),
395
+ )
270
396
  : 0;
271
397
  const maxScrollOffset = Math.max(0, totalLines - pageSize);
272
398
  const thumbStart =
@@ -275,9 +401,9 @@ export async function showTextPreview(
275
401
  : 0;
276
402
  scrollbarHitbox = scrollable
277
403
  ? {
278
- col: overlayLeft + width - 1,
279
- startRow: overlayTop + 4,
280
- endRow: overlayTop + 3 + pageSize,
404
+ col: width - 2,
405
+ startRow: 3,
406
+ endRow: 2 + pageSize,
281
407
  thumbStart,
282
408
  thumbSize,
283
409
  maxOffset: maxScrollOffset,
@@ -286,7 +412,10 @@ export async function showTextPreview(
286
412
  const scrollbar = (row: number): string => {
287
413
  if (!scrollable) return " ";
288
414
  const inThumb = row >= thumbStart && row < thumbStart + thumbSize;
289
- return theme.fg(inThumb ? "accent" : "borderMuted", inThumb ? "█" : "│");
415
+ // 对齐官方:thumb 常态 ┃、hover/拖动中 █;track 始终 │。
416
+ const thumbActive = scrollbarHovered || scrollbarDragOffset !== null;
417
+ if (inThumb) return theme.fg("accent", thumbActive ? "█" : "┃");
418
+ return theme.fg("borderMuted", "│");
290
419
  };
291
420
  const bodyRows = Array.from({ length: pageSize }, (_, row) => {
292
421
  const line = visible[row] ?? "";
@@ -294,9 +423,8 @@ export async function showTextPreview(
294
423
  });
295
424
  const start = totalLines === 0 ? 0 : scrollOffset + 1;
296
425
  const end = Math.min(totalLines, scrollOffset + pageSize);
297
- const status = `${start}-${end} / ${totalLines} lines · ↑↓ PgUp/PgDn Home/End · [copy] · [esc] close`;
298
-
299
- return [
426
+ const status = `${start}-${end} / ${totalLines} lines · ↑↓ PgUp/PgDn Home/End`;
427
+ const lines = [
300
428
  border(`╭${"─".repeat(inner)}╮`),
301
429
  `${border("│")}${padLine(` ${theme.bold(theme.fg("accent", title))}`, inner - actionsWidth)}${theme.fg(copyHovered ? "text" : "muted", COPY_LABEL)} ${theme.fg(escHovered ? "text" : "muted", ESC_LABEL)}${border("│")}`,
302
430
  `${border("├")}${border("─".repeat(inner))}${border("┤")}`,
@@ -305,6 +433,8 @@ export async function showTextPreview(
305
433
  `${border("│")}${padLine(theme.fg("dim", ` ${status}`), inner)}${border("│")}`,
306
434
  border(`╰${"─".repeat(inner)}╯`),
307
435
  ];
436
+ screenOrigin = centeredOverlayOrigin(tui, 2, 80, width, lines.length);
437
+ return lines;
308
438
  },
309
439
  };
310
440
  },
@@ -312,7 +442,7 @@ export async function showTextPreview(
312
442
  overlay: true,
313
443
  overlayOptions: {
314
444
  anchor: "center",
315
- width: "85%",
445
+ width: "80%",
316
446
  minWidth: 50,
317
447
  maxHeight: "80%",
318
448
  margin: 2,
@@ -330,19 +460,15 @@ const tokenEstimate = (value: unknown): number => {
330
460
  return Math.max(0, Math.ceil(text.length / 4));
331
461
  };
332
462
 
333
- /** system 消息对请求的贡献:正文 + 各 section 文本 + 声明的工具 schema,与 pi 的 estimateTokens 同口径。 */
463
+ /** 只算会进请求正文的 content 和 sections。工具 schema 走 tools 参数,由 Tools definition 单独计一次。 */
334
464
  function systemMessageTokens(message: {
335
465
  content?: unknown;
336
466
  sections?: Record<string, string | null> | null;
337
- toolsAdded?: unknown;
338
- toolsRemoved?: unknown;
339
467
  }): number {
340
468
  let chars = typeof message.content === "string" ? message.content.length : 0;
341
469
  for (const section of Object.values(message.sections ?? {})) {
342
470
  if (typeof section === "string") chars += section.length;
343
471
  }
344
- if (message.toolsAdded) chars += JSON.stringify(message.toolsAdded).length;
345
- if (message.toolsRemoved) chars += JSON.stringify(message.toolsRemoved).length;
346
472
  return Math.ceil(chars / 4);
347
473
  }
348
474
 
@@ -527,12 +653,16 @@ export function collectContextBreakdown(
527
653
  systemTokens = Math.max(0, systemTokens);
528
654
  return {
529
655
  parts: [
530
- { label: "System prompt", tokens: systemTokens, color: "accent" },
531
- { label: "Memory", tokens: memoryTokens, color: "error" },
532
- { label: "Skills", tokens: skillsTokens, color: "warning" },
533
- { label: "Tools definition", tokens: toolDefinitionTokens, color: "success" },
534
- { label: "Tool results", tokens: toolResultTokens, color: "customMessageLabel" },
535
- { label: "Context", tokens: contextTokens, color: "warning" },
656
+ { label: "System prompt", tokens: systemTokens, color: rgbColor(0x79, 0xc0, 0xff) },
657
+ { label: "Memory", tokens: memoryTokens, color: rgbColor(0xff, 0x7b, 0x72) },
658
+ { label: "Skills", tokens: skillsTokens, color: rgbColor(0xcb, 0x92, 0x14) },
659
+ {
660
+ label: "Tools definition",
661
+ tokens: toolDefinitionTokens,
662
+ color: rgbColor(0x37, 0xb3, 0x4a),
663
+ },
664
+ { label: "Tool results", tokens: toolResultTokens, color: rgbColor(0xf7, 0x78, 0xba) },
665
+ { label: "Context", tokens: contextTokens, color: rgbColor(0xf0, 0x88, 0x3e) },
536
666
  ] satisfies ContextPart[],
537
667
  previews: {
538
668
  systemPrompt:
@@ -566,8 +696,8 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
566
696
  const free = Math.max(0, contextWindow - used);
567
697
  const allParts = [
568
698
  ...parts,
569
- { label: "Other", tokens: other, color: "muted" as const },
570
- { label: "Free space", tokens: free, color: "dim" as const },
699
+ { label: "Other", tokens: other, color: rgbColor(0x92, 0x9e, 0xab) },
700
+ { label: "Free space", tokens: free, color: rgbColor(0x76, 0x84, 0x94) },
571
701
  ];
572
702
 
573
703
  if (ctx.mode !== "tui") {
@@ -641,6 +771,60 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
641
771
  let escHitbox: { row: number; startCol: number; endCol: number } | undefined;
642
772
  let escHovered = false;
643
773
  let hoveredKey: PreviewKey | undefined;
774
+ let screenOrigin = { col: 0, row: 0 };
775
+
776
+ /** context 主弹框鼠标共用处:handleInput(raw SGR)与 handleMouse(overlay
777
+ * 事件对象)同一套 hover/选中/关闭行为。返回 true 表示被预览消费。 */
778
+ const handleDialogMouse = (args: {
779
+ col: number;
780
+ row: number;
781
+ release: boolean;
782
+ motion: boolean;
783
+ baseButton: number;
784
+ }): boolean => {
785
+ const { col, row, release, motion, baseButton } = args;
786
+ if (release) return true;
787
+ if (motion) {
788
+ const overEsc = Boolean(
789
+ escHitbox &&
790
+ row === escHitbox.row &&
791
+ col >= escHitbox.startCol &&
792
+ col <= escHitbox.endCol,
793
+ );
794
+ const hovered = previewHitboxes.find(
795
+ (candidate) =>
796
+ row === candidate.row && col >= candidate.startCol && col <= candidate.endCol,
797
+ );
798
+ if (overEsc !== escHovered || hovered?.key !== hoveredKey) {
799
+ escHovered = overEsc;
800
+ hoveredKey = hovered?.key;
801
+ tui.requestRender();
802
+ }
803
+ return true;
804
+ }
805
+ if (baseButton !== 0) return true;
806
+ if (
807
+ escHitbox &&
808
+ row === escHitbox.row &&
809
+ col >= escHitbox.startCol &&
810
+ col <= escHitbox.endCol
811
+ ) {
812
+ done(undefined);
813
+ return true;
814
+ }
815
+ const hitbox = previewHitboxes.find(
816
+ (candidate) =>
817
+ row === candidate.row && col >= candidate.startCol && col <= candidate.endCol,
818
+ );
819
+ if (hitbox) {
820
+ selectedPreviewIndex = Math.max(
821
+ 0,
822
+ visiblePreviews.findIndex((preview) => preview.key === hitbox.key),
823
+ );
824
+ done(hitbox.key);
825
+ }
826
+ return true;
827
+ };
644
828
 
645
829
  return {
646
830
  invalidate() {},
@@ -664,53 +848,23 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
664
848
  done(visiblePreviews[selectedPreviewIndex]?.key);
665
849
  return;
666
850
  }
667
-
668
851
  const mouse = parseSgrMousePacket(data);
669
- if (!mouse || mouse.final !== "M") return;
670
- if ((mouse.code & 32) !== 0) {
671
- // SGR 1003 的无按键 hover code 为 35,不能按左键事件过滤。
672
- const overEsc = Boolean(
673
- escHitbox &&
674
- mouse.row === escHitbox.row &&
675
- mouse.col >= escHitbox.startCol &&
676
- mouse.col <= escHitbox.endCol,
677
- );
678
- const hovered = previewHitboxes.find(
679
- (candidate) =>
680
- mouse.row === candidate.row &&
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
- }
852
+ if (!mouse) return;
853
+ handleDialogMouse(localMouseFromSgr(tui, mouse, screenOrigin));
854
+ },
855
+ // overlay 事件对象路径:event.x/y 是弹框局部 0-based 坐标,
856
+ // 与 hitbox 同基准。click 是 press+release 的合成,动作已在
857
+ // press 做过,只消费。
858
+ handleMouse(event: any) {
859
+ if (event.type === "click") return { handled: true };
860
+ handleDialogMouse({
861
+ col: Number(event.x),
862
+ row: Number(event.y),
863
+ release: event.type === "release",
864
+ motion: event.type === "move" || event.type === "drag",
865
+ baseButton: event.button === "left" ? 0 : -1,
866
+ });
867
+ return { handled: true };
714
868
  },
715
869
  render(width: number) {
716
870
  const inner = Math.max(1, width - 2);
@@ -730,7 +884,7 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
730
884
  Math.round((part.tokens / Math.max(1, contextWindow)) * barWidth),
731
885
  );
732
886
  remaining -= cells;
733
- return theme.fg(part.color, "█".repeat(Math.max(0, cells)));
887
+ return theme.style("█".repeat(Math.max(0, cells)), { fg: part.color });
734
888
  })
735
889
  .join("");
736
890
  const labelWidth = Math.min(
@@ -740,7 +894,7 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
740
894
  const selectedLabel = visiblePreviews[selectedPreviewIndex]?.label;
741
895
  const partRows = allParts.map((part) => {
742
896
  const pct = contextWindow > 0 ? (part.tokens / contextWindow) * 100 : 0;
743
- const swatch = theme.fg(part.color, "■");
897
+ const swatch = theme.style("■", { fg: part.color });
744
898
  const label = part.label.padEnd(labelWidth);
745
899
  const amount = `${formatTokens(part.tokens).padStart(7)} ${pct.toFixed(1).padStart(5)}%`;
746
900
  const selected = part.label === selectedLabel;
@@ -769,10 +923,10 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
769
923
  Math.max(1, terminalHeight - 2),
770
924
  );
771
925
  const visibleHeight = Math.min(lines.length, maxHeight);
772
- const overlayTop =
773
- 1 + Math.floor((Math.max(1, terminalHeight - 2) - visibleHeight) / 2);
774
- const overlayLeft = Math.floor((Math.max(1, tui.terminal.columns) - width) / 2);
775
- escHitbox = escCloseHitbox({ left: overlayLeft, top: overlayTop, width });
926
+ // hitbox 用弹框局部坐标(0-based):esc 在 title 行(第 1 行)
927
+ // 尾部 │ 前 5 列,preview 行对应 partRows(顶 border=0、title=1、
928
+ // ├=2、segments=3、空行=4,part 从 5 起)。不依赖官方放置行列。
929
+ escHitbox = { row: 1, startCol: width - 6, endCol: width - 2 };
776
930
  previewHitboxes = visiblePreviews.flatMap((preview) => {
777
931
  const partIndex = allParts.findIndex((part) => part.label === preview.label);
778
932
  const line = 5 + partIndex;
@@ -780,13 +934,14 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
780
934
  ? [
781
935
  {
782
936
  key: preview.key,
783
- row: overlayTop + line + 1,
784
- startCol: overlayLeft + 1,
785
- endCol: overlayLeft + width,
937
+ row: line,
938
+ startCol: 0,
939
+ endCol: width - 1,
786
940
  },
787
941
  ]
788
942
  : [];
789
943
  });
944
+ screenOrigin = centeredOverlayOrigin(tui, 1, 90, width, visibleHeight);
790
945
 
791
946
  return lines;
792
947
  },