pi-cc-extensions 1.0.2 → 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.
- package/README.en.md +35 -35
- package/README.md +3 -3
- package/extensions/config/config.ts +5 -0
- package/extensions/config/panel.ts +22 -0
- package/extensions/feature/context.ts +339 -149
- package/extensions/feature/reference/index.ts +2 -0
- package/extensions/feature/reference/subagent.ts +2 -0
- package/extensions/feature/shell/footer.ts +1 -0
- package/extensions/feature/shell/token-rate.ts +15 -3
- package/extensions/feature/shell/working-message.ts +0 -19
- package/extensions/renderer/compact-mode.ts +152 -49
- package/extensions/renderer/index.ts +3 -0
- package/extensions/renderer/mouse/interaction.ts +53 -26
- package/extensions/renderer/tool/diff/write-execution.ts +2 -2
- package/package.json +1 -1
|
@@ -3,18 +3,20 @@ import {
|
|
|
3
3
|
type ExtensionAPI,
|
|
4
4
|
type ExtensionCommandContext,
|
|
5
5
|
type ToolInfo,
|
|
6
|
+
copyToClipboard,
|
|
6
7
|
estimateTokens,
|
|
7
8
|
formatSkillsForPrompt,
|
|
8
9
|
getMarkdownTheme,
|
|
9
10
|
} from "@earendil-works/pi-coding-agent";
|
|
10
11
|
import { Key, Markdown, matchesKey, visibleWidth } from "@earendil-works/pi-tui";
|
|
11
|
-
import {
|
|
12
|
+
import { type Color, rgbColor } from "@earendil-works/pi-tui";
|
|
12
13
|
import { padLine } from "../utils/format.ts";
|
|
13
14
|
|
|
14
15
|
export type ContextPart = {
|
|
15
16
|
label: string;
|
|
16
17
|
tokens: number;
|
|
17
|
-
|
|
18
|
+
/** 固定 RGB 色值,不走主题 token——避免不同主题撞色。渲染用 theme.style({fg: color})。 */
|
|
19
|
+
color: Color;
|
|
18
20
|
};
|
|
19
21
|
|
|
20
22
|
type PreviewKey =
|
|
@@ -54,6 +56,30 @@ export function escCloseHitbox(bounds: DialogBounds): {
|
|
|
54
56
|
};
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
const COPY_LABEL = "[copy]";
|
|
60
|
+
const ESC_LABEL = "[esc]";
|
|
61
|
+
|
|
62
|
+
/** 测试可替换;生产使用 pi 的剪贴板实现。 */
|
|
63
|
+
let copyTextImpl: (text: string) => Promise<void> = (text) => copyToClipboard(text);
|
|
64
|
+
|
|
65
|
+
export function setPreviewCopyImpl(impl: ((text: string) => Promise<void>) | null): void {
|
|
66
|
+
copyTextImpl = impl ?? ((text) => copyToClipboard(text));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 1-based terminal hitbox of the [copy] button, one space left of [esc] on the same title row. */
|
|
70
|
+
export function copyButtonHitbox(bounds: DialogBounds): {
|
|
71
|
+
row: number;
|
|
72
|
+
startCol: number;
|
|
73
|
+
endCol: number;
|
|
74
|
+
} {
|
|
75
|
+
const esc = escCloseHitbox(bounds);
|
|
76
|
+
return {
|
|
77
|
+
row: esc.row,
|
|
78
|
+
startCol: esc.startCol - visibleWidth(COPY_LABEL) - 1,
|
|
79
|
+
endCol: esc.startCol - 2,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
57
83
|
let activeContextOverlays = 0;
|
|
58
84
|
|
|
59
85
|
/** fullscreen 输入包装用于把鼠标事件继续传给当前 context 主弹框或文本预览 overlay。 */
|
|
@@ -81,7 +107,10 @@ export async function showTextPreview(
|
|
|
81
107
|
let pageSize = 1;
|
|
82
108
|
let totalLines = 1;
|
|
83
109
|
let escHovered = false;
|
|
110
|
+
let copyHovered = false;
|
|
111
|
+
let scrollbarHovered = false;
|
|
84
112
|
let escHitbox: { row: number; startCol: number; endCol: number } | undefined;
|
|
113
|
+
let copyHitbox: { row: number; startCol: number; endCol: number } | undefined;
|
|
85
114
|
let scrollbarHitbox:
|
|
86
115
|
| {
|
|
87
116
|
col: number;
|
|
@@ -108,6 +137,30 @@ export async function showTextPreview(
|
|
|
108
137
|
tui.requestRender();
|
|
109
138
|
};
|
|
110
139
|
|
|
140
|
+
const setCopyHovered = (hovered: boolean): void => {
|
|
141
|
+
if (hovered === copyHovered) return;
|
|
142
|
+
copyHovered = hovered;
|
|
143
|
+
tui.requestRender();
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const setScrollbarHovered = (hovered: boolean): void => {
|
|
147
|
+
if (hovered === scrollbarHovered) return;
|
|
148
|
+
scrollbarHovered = hovered;
|
|
149
|
+
tui.requestRender();
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const copyPreview = (): void => {
|
|
153
|
+
void copyTextImpl(content)
|
|
154
|
+
.then(() => {
|
|
155
|
+
const flash = (tui as { flash?: (message: string) => void }).flash;
|
|
156
|
+
if (typeof flash === "function") flash("Copied!");
|
|
157
|
+
else ctx.ui.notify("Copied preview to clipboard", "info");
|
|
158
|
+
})
|
|
159
|
+
.catch((error: unknown) => {
|
|
160
|
+
ctx.ui.notify(error instanceof Error ? error.message : "Copy failed", "error");
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
|
|
111
164
|
const dragScrollbarTo = (mouseRow: number): void => {
|
|
112
165
|
if (!scrollbarHitbox || scrollbarDragOffset === null) return;
|
|
113
166
|
const trackSize = scrollbarHitbox.endRow - scrollbarHitbox.startRow + 1;
|
|
@@ -123,6 +176,92 @@ export async function showTextPreview(
|
|
|
123
176
|
scrollTo(nextOffset);
|
|
124
177
|
};
|
|
125
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
|
+
|
|
126
265
|
return {
|
|
127
266
|
invalidate() {
|
|
128
267
|
markdownView.invalidate();
|
|
@@ -138,56 +277,29 @@ export async function showTextPreview(
|
|
|
138
277
|
else if (matchesKey(data, "pageDown")) scrollTo(scrollOffset + pageSize);
|
|
139
278
|
else if (matchesKey(data, Key.home)) scrollTo(0);
|
|
140
279
|
else if (matchesKey(data, Key.end)) scrollTo(totalLines - pageSize);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
done(undefined);
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
if (
|
|
163
|
-
scrollbarHitbox &&
|
|
164
|
-
mouse.col === scrollbarHitbox.col &&
|
|
165
|
-
mouse.row >= scrollbarHitbox.startRow &&
|
|
166
|
-
mouse.row <= scrollbarHitbox.endRow
|
|
167
|
-
) {
|
|
168
|
-
const trackRow = mouse.row - scrollbarHitbox.startRow;
|
|
169
|
-
const inThumb =
|
|
170
|
-
trackRow >= scrollbarHitbox.thumbStart &&
|
|
171
|
-
trackRow < scrollbarHitbox.thumbStart + scrollbarHitbox.thumbSize;
|
|
172
|
-
scrollbarDragOffset = inThumb
|
|
173
|
-
? trackRow - scrollbarHitbox.thumbStart
|
|
174
|
-
: Math.floor(scrollbarHitbox.thumbSize / 2);
|
|
175
|
-
dragScrollbarTo(mouse.row);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
if (motion && scrollbarDragOffset !== null) {
|
|
180
|
-
dragScrollbarTo(mouse.row);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
if (button === 64) scrollTo(scrollOffset - 3);
|
|
184
|
-
else if (button === 65) scrollTo(scrollOffset + 3);
|
|
185
|
-
}
|
|
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 };
|
|
186
298
|
},
|
|
187
299
|
|
|
188
300
|
render(width: number) {
|
|
189
301
|
const inner = Math.max(1, width - 2);
|
|
190
|
-
const
|
|
302
|
+
const actionsWidth = visibleWidth(`${COPY_LABEL} ${ESC_LABEL}`);
|
|
191
303
|
const bodyInner = Math.max(1, inner - 1);
|
|
192
304
|
const bodyWidth = Math.max(1, bodyInner - 1);
|
|
193
305
|
const terminalHeight = Math.max(1, tui.terminal.rows);
|
|
@@ -201,15 +313,24 @@ export async function showTextPreview(
|
|
|
201
313
|
const wrapped = markdownView.render(bodyWidth);
|
|
202
314
|
totalLines = wrapped.length;
|
|
203
315
|
scrollOffset = Math.min(scrollOffset, Math.max(0, totalLines - pageSize));
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
+
};
|
|
208
324
|
const visible = wrapped.slice(scrollOffset, scrollOffset + pageSize);
|
|
209
325
|
const border = (text: string) => theme.fg("border", text);
|
|
210
326
|
const scrollable = totalLines > pageSize;
|
|
327
|
+
// thumb 几何对齐官方 getScrollbarGeometry:trackHeight=pageSize、
|
|
328
|
+
// contentHeight=totalLines,minThumb=min(2,track),thumb=round(track²/content)。
|
|
211
329
|
const thumbSize = scrollable
|
|
212
|
-
? Math.max(
|
|
330
|
+
? Math.max(
|
|
331
|
+
Math.min(2, pageSize),
|
|
332
|
+
Math.min(pageSize, Math.round((pageSize * pageSize) / totalLines)),
|
|
333
|
+
)
|
|
213
334
|
: 0;
|
|
214
335
|
const maxScrollOffset = Math.max(0, totalLines - pageSize);
|
|
215
336
|
const thumbStart =
|
|
@@ -218,9 +339,9 @@ export async function showTextPreview(
|
|
|
218
339
|
: 0;
|
|
219
340
|
scrollbarHitbox = scrollable
|
|
220
341
|
? {
|
|
221
|
-
col:
|
|
222
|
-
startRow:
|
|
223
|
-
endRow:
|
|
342
|
+
col: width - 2,
|
|
343
|
+
startRow: 3,
|
|
344
|
+
endRow: 2 + pageSize,
|
|
224
345
|
thumbStart,
|
|
225
346
|
thumbSize,
|
|
226
347
|
maxOffset: maxScrollOffset,
|
|
@@ -229,7 +350,10 @@ export async function showTextPreview(
|
|
|
229
350
|
const scrollbar = (row: number): string => {
|
|
230
351
|
if (!scrollable) return " ";
|
|
231
352
|
const inThumb = row >= thumbStart && row < thumbStart + thumbSize;
|
|
232
|
-
|
|
353
|
+
// 对齐官方:thumb 常态 ┃、hover/拖动中 █;track 始终 │。
|
|
354
|
+
const thumbActive = scrollbarHovered || scrollbarDragOffset !== null;
|
|
355
|
+
if (inThumb) return theme.fg("accent", thumbActive ? "█" : "┃");
|
|
356
|
+
return theme.fg("borderMuted", "│");
|
|
233
357
|
};
|
|
234
358
|
const bodyRows = Array.from({ length: pageSize }, (_, row) => {
|
|
235
359
|
const line = visible[row] ?? "";
|
|
@@ -237,11 +361,11 @@ export async function showTextPreview(
|
|
|
237
361
|
});
|
|
238
362
|
const start = totalLines === 0 ? 0 : scrollOffset + 1;
|
|
239
363
|
const end = Math.min(totalLines, scrollOffset + pageSize);
|
|
240
|
-
const status = `${start}-${end} / ${totalLines} lines · ↑↓ PgUp/PgDn Home/End
|
|
364
|
+
const status = `${start}-${end} / ${totalLines} lines · ↑↓ PgUp/PgDn Home/End`;
|
|
241
365
|
|
|
242
366
|
return [
|
|
243
367
|
border(`╭${"─".repeat(inner)}╮`),
|
|
244
|
-
`${border("│")}${padLine(` ${theme.bold(theme.fg("accent", title))}`, inner -
|
|
368
|
+
`${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("│")}`,
|
|
245
369
|
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
246
370
|
...bodyRows,
|
|
247
371
|
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
@@ -255,7 +379,7 @@ export async function showTextPreview(
|
|
|
255
379
|
overlay: true,
|
|
256
380
|
overlayOptions: {
|
|
257
381
|
anchor: "center",
|
|
258
|
-
width: "
|
|
382
|
+
width: "80%",
|
|
259
383
|
minWidth: 50,
|
|
260
384
|
maxHeight: "80%",
|
|
261
385
|
margin: 2,
|
|
@@ -273,6 +397,23 @@ const tokenEstimate = (value: unknown): number => {
|
|
|
273
397
|
return Math.max(0, Math.ceil(text.length / 4));
|
|
274
398
|
};
|
|
275
399
|
|
|
400
|
+
/** 只算会进请求正文的 content 和 sections。工具 schema 走 tools 参数,由 Tools definition 单独计一次。 */
|
|
401
|
+
function systemMessageTokens(message: {
|
|
402
|
+
content?: unknown;
|
|
403
|
+
sections?: Record<string, string | null> | null;
|
|
404
|
+
}): number {
|
|
405
|
+
let chars = typeof message.content === "string" ? message.content.length : 0;
|
|
406
|
+
for (const section of Object.values(message.sections ?? {})) {
|
|
407
|
+
if (typeof section === "string") chars += section.length;
|
|
408
|
+
}
|
|
409
|
+
return Math.ceil(chars / 4);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** prompt 组装会把 CRLF 归一成 LF,匹配前对齐行尾。 */
|
|
413
|
+
function normalizeLineEndings(text: string): string {
|
|
414
|
+
return text.replace(/\r\n?/g, "\n");
|
|
415
|
+
}
|
|
416
|
+
|
|
276
417
|
/** 只统计确实嵌进 system prompt 的片段,避免源文件预览把占用加两遍。 */
|
|
277
418
|
function embeddedTokens(prompt: string, chunk: string): number {
|
|
278
419
|
if (!chunk || !prompt.includes(chunk)) return 0;
|
|
@@ -348,24 +489,24 @@ export function collectContextBreakdown(
|
|
|
348
489
|
allTools: ToolInfo[],
|
|
349
490
|
): ContextBreakdown {
|
|
350
491
|
const options = (ctx.getSystemPromptOptions?.() ?? {}) as BuildSystemPromptOptions;
|
|
351
|
-
const systemPrompt = typeof ctx.getSystemPrompt === "function" ? ctx.getSystemPrompt() : "";
|
|
352
492
|
const selectedTools = new Set(options.selectedTools ?? ["read", "bash", "edit", "write"]);
|
|
353
493
|
const toolDefinitionPreview: string[] = [];
|
|
354
494
|
const toolResultPreview: string[] = [];
|
|
355
495
|
const contextPreview: string[] = [];
|
|
496
|
+
const systemPromptPreview: string[] = [];
|
|
356
497
|
let toolDefinitionTokens = 0;
|
|
357
498
|
let toolResultTokens = 0;
|
|
358
499
|
let contextTokens = 0;
|
|
500
|
+
let systemTokens = 0;
|
|
359
501
|
|
|
360
502
|
const memoryPreview: string[] = [];
|
|
361
|
-
|
|
362
|
-
for (const file of
|
|
363
|
-
memoryTokens += embeddedTokens(systemPrompt, file.content);
|
|
503
|
+
const contextFiles = options.contextFiles ?? [];
|
|
504
|
+
for (const file of contextFiles) {
|
|
364
505
|
memoryPreview.push(`## ${file.path}\n\n${previewValue(file.content)}`);
|
|
365
506
|
}
|
|
366
507
|
|
|
367
|
-
const
|
|
368
|
-
const
|
|
508
|
+
const skills = options.skills ?? [];
|
|
509
|
+
const skillsText = formatSkillsForPrompt(skills).trim();
|
|
369
510
|
|
|
370
511
|
for (const tool of allTools) {
|
|
371
512
|
if (!selectedTools.has(tool.name)) continue;
|
|
@@ -378,21 +519,51 @@ export function collectContextBreakdown(
|
|
|
378
519
|
toolDefinitionPreview.push(`## Definition: ${tool.name}\n\n${previewValue(definition)}`);
|
|
379
520
|
}
|
|
380
521
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
522
|
+
let memoryTokens = 0;
|
|
523
|
+
let skillsTokens = 0;
|
|
524
|
+
for (const projected of ctx.sessionManager.buildSessionProjection().entries) {
|
|
525
|
+
const source = projected.sourceEntry;
|
|
526
|
+
for (const message of projected.messages) {
|
|
527
|
+
if (message.role === "system") {
|
|
528
|
+
systemTokens += systemMessageTokens(message);
|
|
529
|
+
// Memory / skills 按投影中的实际 section 文本切分,与 system prompt 增量互补、不重不漏。
|
|
530
|
+
const sections = (message.sections ?? {}) as Record<string, string | null>;
|
|
531
|
+
const projectContext =
|
|
532
|
+
typeof sections.project_context === "string"
|
|
533
|
+
? normalizeLineEndings(sections.project_context)
|
|
534
|
+
: "";
|
|
535
|
+
for (const file of contextFiles) {
|
|
536
|
+
const chunk = `<project_instructions path="${file.path}">\n${normalizeLineEndings(file.content)}\n</project_instructions>`;
|
|
537
|
+
memoryTokens += embeddedTokens(projectContext, chunk);
|
|
538
|
+
systemTokens -= embeddedTokens(projectContext, chunk);
|
|
539
|
+
}
|
|
540
|
+
const skillsSection =
|
|
541
|
+
typeof sections.skills === "string" ? normalizeLineEndings(sections.skills) : "";
|
|
542
|
+
skillsTokens += embeddedTokens(skillsSection, skillsText);
|
|
543
|
+
systemTokens -= embeddedTokens(skillsSection, skillsText);
|
|
544
|
+
systemPromptPreview.push(
|
|
545
|
+
(source.type === "compaction"
|
|
546
|
+
? "## System prompt snapshot at compaction\n\n"
|
|
547
|
+
: "## System prompt update\n\n") +
|
|
548
|
+
previewValue({
|
|
549
|
+
content: message.content,
|
|
550
|
+
sections: message.sections,
|
|
551
|
+
toolsAdded: (message.toolsAdded ?? []).map((tool: { name?: string }) => tool?.name),
|
|
552
|
+
toolsRemoved: (message.toolsRemoved ?? []).map(
|
|
553
|
+
(tool: { name?: string }) => tool?.name,
|
|
554
|
+
),
|
|
555
|
+
}),
|
|
556
|
+
);
|
|
557
|
+
} else if (message.role === "assistant") {
|
|
558
|
+
contextTokens += estimateTokens(message);
|
|
385
559
|
for (const block of message.content) {
|
|
386
560
|
if (block.type === "toolCall") {
|
|
387
|
-
contextTokens += tokenEstimate(block.name) + tokenEstimate(block.arguments);
|
|
388
561
|
contextPreview.push(
|
|
389
562
|
`## Assistant tool call: ${block.name}\n\n${previewValue(block.arguments)}`,
|
|
390
563
|
);
|
|
391
564
|
} else if (block.type === "text") {
|
|
392
|
-
contextTokens += tokenEstimate(block.text);
|
|
393
565
|
contextPreview.push(`## Assistant\n\n${block.text}`);
|
|
394
566
|
} else if (block.type === "thinking") {
|
|
395
|
-
contextTokens += tokenEstimate(block.thinking);
|
|
396
567
|
contextPreview.push(`## Assistant thinking\n\n${block.thinking}`);
|
|
397
568
|
}
|
|
398
569
|
}
|
|
@@ -402,8 +573,8 @@ export function collectContextBreakdown(
|
|
|
402
573
|
`## Result: ${message.toolName}\n\n${previewValue(message.content)}`,
|
|
403
574
|
);
|
|
404
575
|
} else if (message.role === "bashExecution") {
|
|
405
|
-
|
|
406
|
-
|
|
576
|
+
contextTokens += estimateTokens(message);
|
|
577
|
+
contextPreview.push(
|
|
407
578
|
`## Bash\n\nCommand:\n\n${previewValue(message.command)}\n\nOutput:\n\n${previewValue(message.output)}`,
|
|
408
579
|
);
|
|
409
580
|
} else if (message.role === "branchSummary" || message.role === "compactionSummary") {
|
|
@@ -413,29 +584,28 @@ export function collectContextBreakdown(
|
|
|
413
584
|
contextTokens += estimateTokens(message);
|
|
414
585
|
contextPreview.push(`## ${message.role}\n\n${previewValue(message.content)}`);
|
|
415
586
|
}
|
|
416
|
-
} else if (entry.type === "compaction" || entry.type === "branch_summary") {
|
|
417
|
-
contextTokens += tokenEstimate(entry.summary);
|
|
418
|
-
contextPreview.push(
|
|
419
|
-
`## ${entry.type === "compaction" ? "Compaction" : "Branch summary"}\n\n${entry.summary}`,
|
|
420
|
-
);
|
|
421
|
-
} else if (entry.type === "custom_message") {
|
|
422
|
-
contextTokens += tokenEstimate(entry.content);
|
|
423
|
-
contextPreview.push(`## Custom: ${entry.customType}\n\n${previewValue(entry.content)}`);
|
|
424
587
|
}
|
|
425
588
|
}
|
|
426
589
|
|
|
427
|
-
|
|
590
|
+
systemTokens = Math.max(0, systemTokens);
|
|
428
591
|
return {
|
|
429
592
|
parts: [
|
|
430
|
-
{ label: "System prompt", tokens: systemTokens, color:
|
|
431
|
-
{ label: "Memory", tokens: memoryTokens, color:
|
|
432
|
-
{ label: "Skills", tokens: skillsTokens, color:
|
|
433
|
-
{
|
|
434
|
-
|
|
435
|
-
|
|
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) },
|
|
436
603
|
] satisfies ContextPart[],
|
|
437
604
|
previews: {
|
|
438
|
-
systemPrompt:
|
|
605
|
+
systemPrompt:
|
|
606
|
+
systemPromptPreview.join("\n\n") ||
|
|
607
|
+
(typeof ctx.getSystemPrompt === "function" ? ctx.getSystemPrompt() : "") ||
|
|
608
|
+
"No system prompt.",
|
|
439
609
|
memoryFiles: memoryPreview.join("\n\n") || "No memory files in context.",
|
|
440
610
|
skills: skillsText || "No skills in context.",
|
|
441
611
|
tools: toolDefinitionPreview.join("\n\n") || "No active tool definitions.",
|
|
@@ -463,8 +633,8 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
|
|
|
463
633
|
const free = Math.max(0, contextWindow - used);
|
|
464
634
|
const allParts = [
|
|
465
635
|
...parts,
|
|
466
|
-
{ label: "Other", tokens: other, color:
|
|
467
|
-
{ 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) },
|
|
468
638
|
];
|
|
469
639
|
|
|
470
640
|
if (ctx.mode !== "tui") {
|
|
@@ -539,6 +709,59 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
|
|
|
539
709
|
let escHovered = false;
|
|
540
710
|
let hoveredKey: PreviewKey | undefined;
|
|
541
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
|
+
|
|
542
765
|
return {
|
|
543
766
|
invalidate() {},
|
|
544
767
|
handleInput(data: string) {
|
|
@@ -561,53 +784,20 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
|
|
|
561
784
|
done(visiblePreviews[selectedPreviewIndex]?.key);
|
|
562
785
|
return;
|
|
563
786
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
mouse.col >= candidate.startCol &&
|
|
579
|
-
mouse.col <= candidate.endCol,
|
|
580
|
-
);
|
|
581
|
-
if (overEsc !== escHovered || hovered?.key !== hoveredKey) {
|
|
582
|
-
escHovered = overEsc;
|
|
583
|
-
hoveredKey = hovered?.key;
|
|
584
|
-
tui.requestRender();
|
|
585
|
-
}
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
if (mouseBaseButton(mouse.code) !== 0) return;
|
|
589
|
-
if (
|
|
590
|
-
escHitbox &&
|
|
591
|
-
mouse.row === escHitbox.row &&
|
|
592
|
-
mouse.col >= escHitbox.startCol &&
|
|
593
|
-
mouse.col <= escHitbox.endCol
|
|
594
|
-
) {
|
|
595
|
-
done(undefined);
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
598
|
-
const hitbox = previewHitboxes.find(
|
|
599
|
-
(candidate) =>
|
|
600
|
-
mouse.row === candidate.row &&
|
|
601
|
-
mouse.col >= candidate.startCol &&
|
|
602
|
-
mouse.col <= candidate.endCol,
|
|
603
|
-
);
|
|
604
|
-
if (hitbox) {
|
|
605
|
-
selectedPreviewIndex = Math.max(
|
|
606
|
-
0,
|
|
607
|
-
visiblePreviews.findIndex((preview) => preview.key === hitbox.key),
|
|
608
|
-
);
|
|
609
|
-
done(hitbox.key);
|
|
610
|
-
}
|
|
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 };
|
|
611
801
|
},
|
|
612
802
|
render(width: number) {
|
|
613
803
|
const inner = Math.max(1, width - 2);
|
|
@@ -627,7 +817,7 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
|
|
|
627
817
|
Math.round((part.tokens / Math.max(1, contextWindow)) * barWidth),
|
|
628
818
|
);
|
|
629
819
|
remaining -= cells;
|
|
630
|
-
return theme.
|
|
820
|
+
return theme.style("█".repeat(Math.max(0, cells)), { fg: part.color });
|
|
631
821
|
})
|
|
632
822
|
.join("");
|
|
633
823
|
const labelWidth = Math.min(
|
|
@@ -637,7 +827,7 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
|
|
|
637
827
|
const selectedLabel = visiblePreviews[selectedPreviewIndex]?.label;
|
|
638
828
|
const partRows = allParts.map((part) => {
|
|
639
829
|
const pct = contextWindow > 0 ? (part.tokens / contextWindow) * 100 : 0;
|
|
640
|
-
const swatch = theme.fg
|
|
830
|
+
const swatch = theme.style("■", { fg: part.color });
|
|
641
831
|
const label = part.label.padEnd(labelWidth);
|
|
642
832
|
const amount = `${formatTokens(part.tokens).padStart(7)} ${pct.toFixed(1).padStart(5)}%`;
|
|
643
833
|
const selected = part.label === selectedLabel;
|
|
@@ -666,10 +856,10 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
|
|
|
666
856
|
Math.max(1, terminalHeight - 2),
|
|
667
857
|
);
|
|
668
858
|
const visibleHeight = Math.min(lines.length, maxHeight);
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
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 };
|
|
673
863
|
previewHitboxes = visiblePreviews.flatMap((preview) => {
|
|
674
864
|
const partIndex = allParts.findIndex((part) => part.label === preview.label);
|
|
675
865
|
const line = 5 + partIndex;
|
|
@@ -677,9 +867,9 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
|
|
|
677
867
|
? [
|
|
678
868
|
{
|
|
679
869
|
key: preview.key,
|
|
680
|
-
row:
|
|
681
|
-
startCol:
|
|
682
|
-
endCol:
|
|
870
|
+
row: line,
|
|
871
|
+
startCol: 0,
|
|
872
|
+
endCol: width - 1,
|
|
683
873
|
},
|
|
684
874
|
]
|
|
685
875
|
: [];
|