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.
- package/README.en.md +6 -6
- package/README.md +6 -6
- package/extensions/config/config.ts +16 -27
- package/extensions/config/panel.ts +49 -48
- package/extensions/feature/compact-thinking.ts +2 -3
- package/extensions/feature/context.ts +291 -136
- package/extensions/feature/shell/footer-layout.ts +108 -23
- package/extensions/feature/shell/footer.ts +59 -43
- package/extensions/feature/shell/prompt-editor.ts +77 -0
- package/extensions/index.ts +6 -0
- package/extensions/renderer/compact-mode.ts +124 -54
- package/extensions/renderer/default-mode.ts +7 -4
- package/extensions/renderer/mouse/interaction.ts +69 -28
- package/extensions/renderer/mouse/scroll.ts +23 -8
- package/extensions/renderer/mouse/sticky-prompt.ts +2 -2
- package/extensions/renderer/tool/ccstyle-tool.ts +24 -12
- package/extensions/renderer/tool/codemode.ts +121 -6
- package/extensions/renderer/tool/diff/diff-limits.ts +3 -2
- package/extensions/renderer/tool/diff/diff-palette.ts +39 -8
- package/extensions/renderer/tool/diff/diff-presentation.ts +2 -2
- package/extensions/renderer/tool/grouping.ts +13 -13
- package/extensions/renderer/tool/message-display.ts +3 -2
- package/extensions/renderer/tool/names.ts +49 -49
- package/extensions/renderer/tool/result.ts +24 -0
- package/extensions/renderer/tool/tool-state.ts +51 -2
- package/package.json +1 -1
- package/themes/cc-dark.json +72 -71
- package/themes/cc-light.json +72 -71
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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(
|
|
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:
|
|
279
|
-
startRow:
|
|
280
|
-
endRow:
|
|
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
|
-
|
|
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
|
|
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: "
|
|
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
|
-
/**
|
|
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:
|
|
531
|
-
{ label: "Memory", tokens: memoryTokens, color:
|
|
532
|
-
{ label: "Skills", tokens: skillsTokens, color:
|
|
533
|
-
{
|
|
534
|
-
|
|
535
|
-
|
|
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:
|
|
570
|
-
{ label: "Free space", tokens: free, color:
|
|
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
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
escHitbox =
|
|
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:
|
|
784
|
-
startCol:
|
|
785
|
-
endCol:
|
|
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
|
},
|