pi-one-ui 0.7.0 → 0.7.1
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/CHANGELOG.md +25 -1
- package/README.en.md +19 -6
- package/README.md +19 -6
- package/extensions/app/config/context.ts +42 -0
- package/extensions/app/config/editor.ts +80 -0
- package/extensions/app/config/footer.ts +291 -0
- package/extensions/app/config/panel.ts +1 -5
- package/extensions/app/config/renderer.ts +1 -6
- package/extensions/app/config/shell.ts +358 -1435
- package/extensions/app/config/store.ts +22 -1
- package/extensions/app/config/values.ts +33 -0
- package/extensions/app/config/working-line.ts +100 -0
- package/extensions/app/overlay/input-router.ts +56 -32
- package/extensions/app/overlay/overlay-manager.ts +183 -21
- package/extensions/app/runtime/event-coordinator.ts +90 -42
- package/extensions/{features → app/runtime}/flush-docked-bash.ts +12 -2
- package/extensions/app/runtime/render-scheduler.ts +35 -31
- package/extensions/app/runtime/tui-runtime.ts +315 -312
- package/extensions/app/settings/controller.ts +202 -0
- package/extensions/app/settings/panel.ts +81 -0
- package/extensions/app/settings/presets.ts +43 -0
- package/extensions/features/context-inspector/index.ts +83 -662
- package/extensions/layouts/context/index.ts +58 -161
- package/extensions/layouts/context/message/user-message.ts +1 -1
- package/extensions/layouts/context/renderer/index.ts +31 -13
- package/extensions/layouts/context/renderer/mouse/interaction.ts +29 -14
- package/extensions/layouts/editor/controller.ts +9 -18
- package/extensions/layouts/editor/factory.ts +3 -3
- package/extensions/layouts/footer/controller.ts +30 -19
- package/extensions/layouts/footer/footer.ts +2 -2
- package/extensions/layouts/overlay/context-inspector.ts +497 -0
- package/extensions/{app → layouts}/overlay/selector-border.ts +5 -5
- package/extensions/{app → layouts}/overlay/selector-controller.ts +1 -1
- package/extensions/layouts/overlay/settings-panel.ts +359 -0
- package/extensions/{app/commands → layouts/overlay}/settings-previews.ts +4 -4
- package/extensions/layouts/working-line/controller.ts +3 -29
- package/extensions/layouts/working-line/working-line.ts +17 -114
- package/extensions/shared/working-line-text.ts +108 -0
- package/package.json +1 -1
- package/extensions/app/host/pi-extension-port.ts +0 -25
- package/extensions/app/host/pi-ui-port.ts +0 -52
- package/extensions/app/host/tui-capabilities.ts +0 -27
- package/extensions/app/ownership/layout-registry.ts +0 -65
- package/extensions/app/ownership.ts +0 -32
- package/extensions/app/panel.ts +0 -770
- package/extensions/app/presets.ts +0 -35
- package/extensions/app/runtime/runtime-state.ts +0 -83
- /package/extensions/{layouts/working-line → app/config}/working-line-messages.ts +0 -0
- /package/extensions/app/{ownership → runtime}/prototype-patch-registry.ts +0 -0
|
@@ -4,340 +4,33 @@ import {
|
|
|
4
4
|
type ExtensionCommandContext,
|
|
5
5
|
estimateTokens,
|
|
6
6
|
formatSkillsForPrompt,
|
|
7
|
-
getMarkdownTheme,
|
|
8
7
|
type ToolInfo,
|
|
9
8
|
} from "@earendil-works/pi-coding-agent";
|
|
10
9
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
| "success"
|
|
26
|
-
| "warning"
|
|
27
|
-
| "customMessageLabel"
|
|
28
|
-
| "muted"
|
|
29
|
-
| "dim"
|
|
30
|
-
| "error";
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
type PreviewKey =
|
|
34
|
-
| "systemPrompt"
|
|
35
|
-
| "memoryFiles"
|
|
36
|
-
| "skills"
|
|
37
|
-
| "tools"
|
|
38
|
-
| "toolResults"
|
|
39
|
-
| "contextFiles";
|
|
40
|
-
|
|
41
|
-
type ContextPreview = {
|
|
42
|
-
key: PreviewKey;
|
|
43
|
-
label: string;
|
|
44
|
-
title: string;
|
|
45
|
-
content: string;
|
|
10
|
+
type OverlayManager,
|
|
11
|
+
overlayManager,
|
|
12
|
+
} from "../../app/overlay/overlay-manager.ts";
|
|
13
|
+
import {
|
|
14
|
+
type ContextPart,
|
|
15
|
+
type ContextPreviewKey,
|
|
16
|
+
formatTokens,
|
|
17
|
+
showContextInspector,
|
|
18
|
+
} from "../../layouts/overlay/context-inspector.ts";
|
|
19
|
+
|
|
20
|
+
export type { ContextPart } from "../../layouts/overlay/context-inspector.ts";
|
|
21
|
+
export type ContextBreakdown = {
|
|
22
|
+
parts: ContextPart[];
|
|
23
|
+
previews: Record<ContextPreviewKey, string>;
|
|
46
24
|
};
|
|
47
|
-
|
|
48
|
-
function normalizePreviewText(text: string): string {
|
|
49
|
-
return text
|
|
50
|
-
.replace(/\r\n?/g, "\n")
|
|
51
|
-
.replace(/\t/g, " ")
|
|
52
|
-
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export type DialogBounds = { left: number; top: number; width: number };
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Returns the 1-based terminal hitbox of the dialog title's [esc] button.
|
|
59
|
-
*/
|
|
60
|
-
export function escCloseHitbox(bounds: DialogBounds): {
|
|
61
|
-
row: number;
|
|
62
|
-
startCol: number;
|
|
63
|
-
endCol: number;
|
|
64
|
-
} {
|
|
65
|
-
return {
|
|
66
|
-
row: bounds.top + 2,
|
|
67
|
-
startCol: bounds.left + bounds.width - 5,
|
|
68
|
-
endCol: bounds.left + bounds.width - 1,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Reports whether a plugin-owned overlay is currently active.
|
|
74
|
-
*
|
|
75
|
-
* The legacy name is retained because the mouse implementation imports it.
|
|
76
|
-
*/
|
|
77
|
-
export function hasActiveTextPreview(): boolean {
|
|
78
|
-
return overlayManager.hasActive();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Re-enables fullscreen mouse motion after Pi opens an overlay.
|
|
83
|
-
*/
|
|
84
|
-
function ensureFullscreenMouseMotion(tui: any): void {
|
|
85
|
-
if (tui.mode === "fullscreen")
|
|
86
|
-
tui.terminal?.write?.("\x1b[?1003h\x1b[?1006h");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Opens a scrollable overlay for one context preview document.
|
|
91
|
-
*
|
|
92
|
-
* @param ctx Extension command context that provides the custom UI host.
|
|
93
|
-
* @param title Preview title shown in the overlay header.
|
|
94
|
-
* @param rawContent Markdown source displayed in the preview.
|
|
95
|
-
*/
|
|
96
|
-
export async function showTextPreview(
|
|
97
|
-
ctx: Pick<ExtensionCommandContext, "ui">,
|
|
98
|
-
title: string,
|
|
99
|
-
rawContent: string,
|
|
100
|
-
): Promise<void> {
|
|
101
|
-
const content = normalizePreviewText(rawContent);
|
|
102
|
-
await overlayManager.run(() =>
|
|
103
|
-
ctx.ui.custom(
|
|
104
|
-
(tui, theme, _keybindings, done) => {
|
|
105
|
-
ensureFullscreenMouseMotion(tui);
|
|
106
|
-
let scrollOffset = 0;
|
|
107
|
-
let pageSize = 1;
|
|
108
|
-
let totalLines = 1;
|
|
109
|
-
let escHovered = false;
|
|
110
|
-
let escHitbox:
|
|
111
|
-
| { row: number; startCol: number; endCol: number }
|
|
112
|
-
| undefined;
|
|
113
|
-
let scrollbarHitbox:
|
|
114
|
-
| {
|
|
115
|
-
col: number;
|
|
116
|
-
startRow: number;
|
|
117
|
-
endRow: number;
|
|
118
|
-
thumbStart: number;
|
|
119
|
-
thumbSize: number;
|
|
120
|
-
maxOffset: number;
|
|
121
|
-
}
|
|
122
|
-
| undefined;
|
|
123
|
-
let scrollbarDragOffset: number | null = null;
|
|
124
|
-
const markdownView = new Markdown(content, 0, 0, getMarkdownTheme());
|
|
125
|
-
|
|
126
|
-
const scrollTo = (nextOffset: number): void => {
|
|
127
|
-
const next = Math.max(
|
|
128
|
-
0,
|
|
129
|
-
Math.min(nextOffset, Math.max(0, totalLines - pageSize)),
|
|
130
|
-
);
|
|
131
|
-
if (next === scrollOffset) return;
|
|
132
|
-
scrollOffset = next;
|
|
133
|
-
tui.requestRender();
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
const setEscHovered = (hovered: boolean): void => {
|
|
137
|
-
if (hovered === escHovered) return;
|
|
138
|
-
escHovered = hovered;
|
|
139
|
-
tui.requestRender();
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
const dragScrollbarTo = (mouseRow: number): void => {
|
|
143
|
-
if (!scrollbarHitbox || scrollbarDragOffset === null) return;
|
|
144
|
-
const trackSize =
|
|
145
|
-
scrollbarHitbox.endRow - scrollbarHitbox.startRow + 1;
|
|
146
|
-
const maxThumbStart = Math.max(
|
|
147
|
-
0,
|
|
148
|
-
trackSize - scrollbarHitbox.thumbSize,
|
|
149
|
-
);
|
|
150
|
-
const thumbStart = Math.max(
|
|
151
|
-
0,
|
|
152
|
-
Math.min(
|
|
153
|
-
mouseRow - scrollbarHitbox.startRow - scrollbarDragOffset,
|
|
154
|
-
maxThumbStart,
|
|
155
|
-
),
|
|
156
|
-
);
|
|
157
|
-
const nextOffset =
|
|
158
|
-
maxThumbStart > 0
|
|
159
|
-
? Math.round(
|
|
160
|
-
(thumbStart / maxThumbStart) * scrollbarHitbox.maxOffset,
|
|
161
|
-
)
|
|
162
|
-
: 0;
|
|
163
|
-
scrollTo(nextOffset);
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
invalidate() {
|
|
168
|
-
markdownView.invalidate();
|
|
169
|
-
},
|
|
170
|
-
handleInput(data: string) {
|
|
171
|
-
if (
|
|
172
|
-
matchesKey(data, Key.escape) ||
|
|
173
|
-
matchesKey(data, Key.ctrl("c"))
|
|
174
|
-
) {
|
|
175
|
-
done(undefined);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
if (matchesKey(data, Key.up)) scrollTo(scrollOffset - 1);
|
|
179
|
-
else if (matchesKey(data, Key.down)) scrollTo(scrollOffset + 1);
|
|
180
|
-
else if (matchesKey(data, "pageUp"))
|
|
181
|
-
scrollTo(scrollOffset - pageSize);
|
|
182
|
-
else if (matchesKey(data, "pageDown"))
|
|
183
|
-
scrollTo(scrollOffset + pageSize);
|
|
184
|
-
else if (matchesKey(data, Key.home)) scrollTo(0);
|
|
185
|
-
else if (matchesKey(data, Key.end)) scrollTo(totalLines - pageSize);
|
|
186
|
-
else {
|
|
187
|
-
const mouse = parseSgrMousePacket(data);
|
|
188
|
-
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
|
-
setEscHovered(overEsc);
|
|
200
|
-
const button = mouseBaseButton(mouse.code);
|
|
201
|
-
const motion = (mouse.code & 32) !== 0;
|
|
202
|
-
if (button === 0 && !motion) {
|
|
203
|
-
if (overEsc) {
|
|
204
|
-
done(undefined);
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
if (
|
|
208
|
-
scrollbarHitbox &&
|
|
209
|
-
mouse.col === scrollbarHitbox.col &&
|
|
210
|
-
mouse.row >= scrollbarHitbox.startRow &&
|
|
211
|
-
mouse.row <= scrollbarHitbox.endRow
|
|
212
|
-
) {
|
|
213
|
-
const trackRow = mouse.row - scrollbarHitbox.startRow;
|
|
214
|
-
const inThumb =
|
|
215
|
-
trackRow >= scrollbarHitbox.thumbStart &&
|
|
216
|
-
trackRow <
|
|
217
|
-
scrollbarHitbox.thumbStart + scrollbarHitbox.thumbSize;
|
|
218
|
-
scrollbarDragOffset = inThumb
|
|
219
|
-
? trackRow - scrollbarHitbox.thumbStart
|
|
220
|
-
: Math.floor(scrollbarHitbox.thumbSize / 2);
|
|
221
|
-
dragScrollbarTo(mouse.row);
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
if (motion && scrollbarDragOffset !== null) {
|
|
226
|
-
dragScrollbarTo(mouse.row);
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
if (button === 64) scrollTo(scrollOffset - 3);
|
|
230
|
-
else if (button === 65) scrollTo(scrollOffset + 3);
|
|
231
|
-
}
|
|
232
|
-
},
|
|
233
|
-
render(width: number) {
|
|
234
|
-
const inner = Math.max(1, width - 2);
|
|
235
|
-
const escWidth = visibleWidth("[esc]");
|
|
236
|
-
const bodyInner = Math.max(1, inner - 1);
|
|
237
|
-
const bodyWidth = Math.max(1, bodyInner - 1);
|
|
238
|
-
const terminalHeight = Math.max(1, tui.terminal.rows);
|
|
239
|
-
const availableHeight = Math.max(1, terminalHeight - 4);
|
|
240
|
-
const viewportHeight = Math.min(
|
|
241
|
-
30,
|
|
242
|
-
Math.max(1, Math.floor(terminalHeight * 0.8)),
|
|
243
|
-
availableHeight,
|
|
244
|
-
);
|
|
245
|
-
pageSize = Math.max(1, viewportHeight - 6);
|
|
246
|
-
const wrapped = markdownView.render(bodyWidth);
|
|
247
|
-
totalLines = wrapped.length;
|
|
248
|
-
scrollOffset = Math.min(
|
|
249
|
-
scrollOffset,
|
|
250
|
-
Math.max(0, totalLines - pageSize),
|
|
251
|
-
);
|
|
252
|
-
// Centered overlay with margin 2: mirror TUI resolveOverlayLayout for anchor "center".
|
|
253
|
-
const overlayTop =
|
|
254
|
-
2 + Math.floor((availableHeight - viewportHeight) / 2);
|
|
255
|
-
const overlayLeft = Math.floor(
|
|
256
|
-
(Math.max(1, tui.terminal.columns) - width) / 2,
|
|
257
|
-
);
|
|
258
|
-
escHitbox = escCloseHitbox({
|
|
259
|
-
left: overlayLeft,
|
|
260
|
-
top: overlayTop,
|
|
261
|
-
width,
|
|
262
|
-
});
|
|
263
|
-
const visible = wrapped.slice(
|
|
264
|
-
scrollOffset,
|
|
265
|
-
scrollOffset + pageSize,
|
|
266
|
-
);
|
|
267
|
-
const border = (text: string) => theme.fg("border", text);
|
|
268
|
-
const scrollable = totalLines > pageSize;
|
|
269
|
-
const thumbSize = scrollable
|
|
270
|
-
? Math.max(1, Math.floor((pageSize * pageSize) / totalLines))
|
|
271
|
-
: 0;
|
|
272
|
-
const maxScrollOffset = Math.max(0, totalLines - pageSize);
|
|
273
|
-
const thumbStart =
|
|
274
|
-
scrollable && maxScrollOffset > 0
|
|
275
|
-
? Math.round(
|
|
276
|
-
(scrollOffset / maxScrollOffset) * (pageSize - thumbSize),
|
|
277
|
-
)
|
|
278
|
-
: 0;
|
|
279
|
-
scrollbarHitbox = scrollable
|
|
280
|
-
? {
|
|
281
|
-
col: overlayLeft + width - 1,
|
|
282
|
-
startRow: overlayTop + 4,
|
|
283
|
-
endRow: overlayTop + 3 + pageSize,
|
|
284
|
-
thumbStart,
|
|
285
|
-
thumbSize,
|
|
286
|
-
maxOffset: maxScrollOffset,
|
|
287
|
-
}
|
|
288
|
-
: undefined;
|
|
289
|
-
const scrollbar = (row: number): string => {
|
|
290
|
-
if (!scrollable) return " ";
|
|
291
|
-
const inThumb = row >= thumbStart && row < thumbStart + thumbSize;
|
|
292
|
-
return theme.fg(
|
|
293
|
-
inThumb ? "accent" : "borderMuted",
|
|
294
|
-
inThumb ? "█" : "│",
|
|
295
|
-
);
|
|
296
|
-
};
|
|
297
|
-
const bodyRows = Array.from({ length: pageSize }, (_, row) => {
|
|
298
|
-
const line = visible[row] ?? "";
|
|
299
|
-
return `${border("│")}${padLine(` ${line}`, bodyInner)}${scrollbar(row)}${border("│")}`;
|
|
300
|
-
});
|
|
301
|
-
const start = totalLines === 0 ? 0 : scrollOffset + 1;
|
|
302
|
-
const end = Math.min(totalLines, scrollOffset + pageSize);
|
|
303
|
-
const status = `${start}-${end} / ${totalLines} lines · ↑↓ PgUp/PgDn Home/End · [esc] close`;
|
|
304
|
-
|
|
305
|
-
return [
|
|
306
|
-
border(`╭${"─".repeat(inner)}╮`),
|
|
307
|
-
`${border("│")}${padLine(` ${theme.bold(theme.fg("accent", title))}`, inner - escWidth)}${theme.fg(escHovered ? "text" : "muted", "[esc]")}${border("│")}`,
|
|
308
|
-
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
309
|
-
...bodyRows,
|
|
310
|
-
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
311
|
-
`${border("│")}${padLine(theme.fg("dim", ` ${status}`), inner)}${border("│")}`,
|
|
312
|
-
border(`╰${"─".repeat(inner)}╯`),
|
|
313
|
-
];
|
|
314
|
-
},
|
|
315
|
-
};
|
|
316
|
-
},
|
|
317
|
-
{
|
|
318
|
-
overlay: true,
|
|
319
|
-
overlayOptions: {
|
|
320
|
-
anchor: "center",
|
|
321
|
-
width: "85%",
|
|
322
|
-
minWidth: 50,
|
|
323
|
-
maxHeight: "80%",
|
|
324
|
-
margin: 2,
|
|
325
|
-
},
|
|
326
|
-
},
|
|
327
|
-
),
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
25
|
const tokenEstimate = (value: unknown): number => {
|
|
332
|
-
if (!value)
|
|
26
|
+
if (!value) {
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
333
29
|
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
334
30
|
return Math.max(0, Math.ceil(text.length / 4));
|
|
335
31
|
};
|
|
336
|
-
|
|
337
|
-
/** 只统计确实嵌进 system prompt 的片段,避免源文件预览把占用加两遍。 */
|
|
338
32
|
function embeddedTokens(prompt: string, chunk: string): number {
|
|
339
|
-
|
|
340
|
-
return tokenEstimate(chunk);
|
|
33
|
+
return chunk && prompt.includes(chunk) ? tokenEstimate(chunk) : 0;
|
|
341
34
|
}
|
|
342
35
|
|
|
343
36
|
export function capParts(
|
|
@@ -347,33 +40,32 @@ export function capParts(
|
|
|
347
40
|
): ContextPart[] {
|
|
348
41
|
const fixed = parts.slice(0, fixedPrefix);
|
|
349
42
|
const variable = parts.slice(fixedPrefix);
|
|
350
|
-
const
|
|
351
|
-
|
|
43
|
+
const variableTarget = Math.max(
|
|
44
|
+
0,
|
|
45
|
+
target - fixed.reduce((sum, part) => sum + part.tokens, 0),
|
|
46
|
+
);
|
|
352
47
|
const estimated = variable.reduce((sum, part) => sum + part.tokens, 0);
|
|
353
|
-
if (estimated <= variableTarget || estimated === 0)
|
|
48
|
+
if (estimated <= variableTarget || estimated === 0) {
|
|
49
|
+
return parts;
|
|
50
|
+
}
|
|
354
51
|
if (variableTarget === 0) {
|
|
355
52
|
return [...fixed, ...variable.map((part) => ({ ...part, tokens: 0 }))];
|
|
356
53
|
}
|
|
357
|
-
|
|
358
54
|
let previous = 0;
|
|
359
55
|
let cumulative = 0;
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
export function formatTokens(tokens: number): string {
|
|
374
|
-
if (tokens < 1_000) return String(tokens);
|
|
375
|
-
if (tokens < 100_000) return `${(tokens / 1_000).toFixed(1)}k`;
|
|
376
|
-
return `${Math.round(tokens / 1_000)}k`;
|
|
56
|
+
return [
|
|
57
|
+
...fixed,
|
|
58
|
+
...variable.map((part, index) => {
|
|
59
|
+
cumulative += part.tokens;
|
|
60
|
+
const next =
|
|
61
|
+
index === variable.length - 1
|
|
62
|
+
? variableTarget
|
|
63
|
+
: Math.round((cumulative / estimated) * variableTarget);
|
|
64
|
+
const tokens = next - previous;
|
|
65
|
+
previous = next;
|
|
66
|
+
return { ...part, tokens };
|
|
67
|
+
}),
|
|
68
|
+
];
|
|
377
69
|
}
|
|
378
70
|
|
|
379
71
|
export function resolveUsedTokens(
|
|
@@ -392,26 +84,21 @@ export function resolveUsedTokens(
|
|
|
392
84
|
reported !== undefined &&
|
|
393
85
|
fromPercent !== undefined
|
|
394
86
|
) {
|
|
395
|
-
// 某些 Provider 会返回异常 totalTokens;百分比与底部状态栏不一致时优先采用百分比。
|
|
396
87
|
const tolerance = Math.max(32, Math.round(contextWindow * 0.001));
|
|
397
|
-
if (Math.abs(reported - fromPercent) > tolerance)
|
|
88
|
+
if (Math.abs(reported - fromPercent) > tolerance) {
|
|
89
|
+
resolved = fromPercent;
|
|
90
|
+
}
|
|
398
91
|
}
|
|
399
|
-
|
|
400
|
-
if (estimated > 0 && resolved < estimated * 0.25) return estimated;
|
|
401
|
-
return resolved;
|
|
92
|
+
return estimated > 0 && resolved < estimated * 0.25 ? estimated : resolved;
|
|
402
93
|
}
|
|
403
94
|
|
|
404
|
-
type ContextBreakdown = {
|
|
405
|
-
parts: ContextPart[];
|
|
406
|
-
previews: Record<PreviewKey, string>;
|
|
407
|
-
};
|
|
408
|
-
|
|
409
95
|
function previewValue(value: unknown): string {
|
|
410
|
-
|
|
411
|
-
|
|
96
|
+
return typeof value === "string"
|
|
97
|
+
? value
|
|
98
|
+
: `\`\`\`json\n${JSON.stringify(value, null, 2)}\n\`\`\``;
|
|
412
99
|
}
|
|
413
100
|
|
|
414
|
-
/**
|
|
101
|
+
/** 按当前请求的 system prompt、tools 和 messages 计算数据与预览。 */
|
|
415
102
|
export function collectContextBreakdown(
|
|
416
103
|
ctx: ExtensionCommandContext,
|
|
417
104
|
allTools: ToolInfo[],
|
|
@@ -426,22 +113,21 @@ export function collectContextBreakdown(
|
|
|
426
113
|
const toolDefinitionPreview: string[] = [];
|
|
427
114
|
const toolResultPreview: string[] = [];
|
|
428
115
|
const contextPreview: string[] = [];
|
|
116
|
+
const memoryPreview: string[] = [];
|
|
429
117
|
let toolDefinitionTokens = 0;
|
|
430
118
|
let toolResultTokens = 0;
|
|
431
119
|
let contextTokens = 0;
|
|
432
|
-
|
|
433
|
-
const memoryPreview: string[] = [];
|
|
434
120
|
let memoryTokens = 0;
|
|
435
121
|
for (const file of options.contextFiles ?? []) {
|
|
436
122
|
memoryTokens += embeddedTokens(systemPrompt, file.content);
|
|
437
123
|
memoryPreview.push(`## ${file.path}\n\n${previewValue(file.content)}`);
|
|
438
124
|
}
|
|
439
|
-
|
|
440
125
|
const skillsText = formatSkillsForPrompt(options.skills ?? []).trim();
|
|
441
126
|
const skillsTokens = embeddedTokens(systemPrompt, skillsText);
|
|
442
|
-
|
|
443
127
|
for (const tool of allTools) {
|
|
444
|
-
if (!selectedTools.has(tool.name))
|
|
128
|
+
if (!selectedTools.has(tool.name)) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
445
131
|
const definition = {
|
|
446
132
|
name: tool.name,
|
|
447
133
|
description: tool.description,
|
|
@@ -452,7 +138,6 @@ export function collectContextBreakdown(
|
|
|
452
138
|
`## Definition: ${tool.name}\n\n${previewValue(definition)}`,
|
|
453
139
|
);
|
|
454
140
|
}
|
|
455
|
-
|
|
456
141
|
for (const entry of ctx.sessionManager.buildContextEntries()) {
|
|
457
142
|
if (entry.type === "message") {
|
|
458
143
|
const message = entry.message;
|
|
@@ -506,14 +191,16 @@ export function collectContextBreakdown(
|
|
|
506
191
|
);
|
|
507
192
|
}
|
|
508
193
|
}
|
|
509
|
-
|
|
510
|
-
const systemTokens = Math.max(
|
|
511
|
-
0,
|
|
512
|
-
tokenEstimate(systemPrompt) - memoryTokens - skillsTokens,
|
|
513
|
-
);
|
|
514
194
|
return {
|
|
515
195
|
parts: [
|
|
516
|
-
{
|
|
196
|
+
{
|
|
197
|
+
label: "System prompt",
|
|
198
|
+
tokens: Math.max(
|
|
199
|
+
0,
|
|
200
|
+
tokenEstimate(systemPrompt) - memoryTokens - skillsTokens,
|
|
201
|
+
),
|
|
202
|
+
color: "accent",
|
|
203
|
+
},
|
|
517
204
|
{ label: "Memory", tokens: memoryTokens, color: "error" },
|
|
518
205
|
{ label: "Skills", tokens: skillsTokens, color: "warning" },
|
|
519
206
|
{
|
|
@@ -527,7 +214,7 @@ export function collectContextBreakdown(
|
|
|
527
214
|
color: "customMessageLabel",
|
|
528
215
|
},
|
|
529
216
|
{ label: "Context", tokens: contextTokens, color: "warning" },
|
|
530
|
-
]
|
|
217
|
+
],
|
|
531
218
|
previews: {
|
|
532
219
|
systemPrompt: systemPrompt || "No system prompt.",
|
|
533
220
|
memoryFiles: memoryPreview.join("\n\n") || "No memory files in context.",
|
|
@@ -542,20 +229,21 @@ export function collectContextBreakdown(
|
|
|
542
229
|
};
|
|
543
230
|
}
|
|
544
231
|
|
|
545
|
-
export default function contextUsageExtension(
|
|
232
|
+
export default function contextUsageExtension(
|
|
233
|
+
pi: ExtensionAPI,
|
|
234
|
+
overlays: OverlayManager = overlayManager,
|
|
235
|
+
): void {
|
|
546
236
|
pi.registerCommand("context", {
|
|
547
237
|
description: "Show the current context-window distribution",
|
|
548
238
|
handler: async (_args, ctx) => {
|
|
549
239
|
const usage = ctx.getContextUsage();
|
|
550
240
|
const contextWindow =
|
|
551
241
|
usage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
|
|
552
|
-
const
|
|
553
|
-
const breakdown = collectContextBreakdown(ctx, tools);
|
|
242
|
+
const breakdown = collectContextBreakdown(ctx, pi.getAllTools());
|
|
554
243
|
const estimated = breakdown.parts.reduce(
|
|
555
244
|
(sum, part) => sum + part.tokens,
|
|
556
245
|
0,
|
|
557
246
|
);
|
|
558
|
-
// System / Memory / Skills / Tools definition 为固定项,capParts 时保持原值不压缩。
|
|
559
247
|
const fixedTokens = breakdown.parts
|
|
560
248
|
.slice(0, 4)
|
|
561
249
|
.reduce((sum, part) => sum + part.tokens, 0);
|
|
@@ -565,300 +253,33 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
|
|
|
565
253
|
);
|
|
566
254
|
const parts = capParts(breakdown.parts, used, 4);
|
|
567
255
|
const attributed = parts.reduce((sum, part) => sum + part.tokens, 0);
|
|
568
|
-
const
|
|
569
|
-
const free = Math.max(0, contextWindow - used);
|
|
570
|
-
const allParts = [
|
|
256
|
+
const allParts: ContextPart[] = [
|
|
571
257
|
...parts,
|
|
572
|
-
{ label: "Other", tokens: other, color: "muted" as const },
|
|
573
|
-
{ label: "Free space", tokens: free, color: "dim" as const },
|
|
574
|
-
];
|
|
575
|
-
|
|
576
|
-
if (ctx.mode !== "tui") {
|
|
577
|
-
const lines = allParts.map(
|
|
578
|
-
(part) => `${part.label}: ${formatTokens(part.tokens)} tokens`,
|
|
579
|
-
);
|
|
580
|
-
ctx.ui.notify(lines.join("\n"), "info");
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
const rawPreviews: ContextPreview[] = [
|
|
585
|
-
{
|
|
586
|
-
key: "systemPrompt",
|
|
587
|
-
label: "System prompt",
|
|
588
|
-
title: "System Prompt",
|
|
589
|
-
content: breakdown.previews.systemPrompt,
|
|
590
|
-
},
|
|
591
258
|
{
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
content: breakdown.previews.memoryFiles,
|
|
259
|
+
label: "Other",
|
|
260
|
+
tokens: Math.max(0, used - attributed),
|
|
261
|
+
color: "muted",
|
|
596
262
|
},
|
|
597
263
|
{
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
content: breakdown.previews.skills,
|
|
602
|
-
},
|
|
603
|
-
{
|
|
604
|
-
key: "tools",
|
|
605
|
-
label: "Tools definition",
|
|
606
|
-
title: "Tools definition",
|
|
607
|
-
content: breakdown.previews.tools,
|
|
608
|
-
},
|
|
609
|
-
{
|
|
610
|
-
key: "toolResults",
|
|
611
|
-
label: "Tool results",
|
|
612
|
-
title: "Tool Results",
|
|
613
|
-
content: breakdown.previews.toolResults,
|
|
614
|
-
},
|
|
615
|
-
{
|
|
616
|
-
key: "contextFiles",
|
|
617
|
-
label: "Context",
|
|
618
|
-
title: "Context",
|
|
619
|
-
content: breakdown.previews.contextFiles,
|
|
264
|
+
label: "Free space",
|
|
265
|
+
tokens: Math.max(0, contextWindow - used),
|
|
266
|
+
color: "dim",
|
|
620
267
|
},
|
|
621
268
|
];
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
);
|
|
629
|
-
const visiblePreviews = previews.filter((preview) =>
|
|
630
|
-
allParts.some((part) => part.label === preview.label),
|
|
631
|
-
);
|
|
632
|
-
let selectedPreviewIndex = 0;
|
|
633
|
-
|
|
634
|
-
while (true) {
|
|
635
|
-
let action;
|
|
636
|
-
action = await overlayManager.run(() =>
|
|
637
|
-
ctx.ui.custom(
|
|
638
|
-
(tui, theme, _keybindings, done) => {
|
|
639
|
-
ensureFullscreenMouseMotion(tui);
|
|
640
|
-
let previewHitboxes: Array<{
|
|
641
|
-
key: PreviewKey;
|
|
642
|
-
row: number;
|
|
643
|
-
startCol: number;
|
|
644
|
-
endCol: number;
|
|
645
|
-
}> = [];
|
|
646
|
-
let escHitbox:
|
|
647
|
-
| { row: number; startCol: number; endCol: number }
|
|
648
|
-
| undefined;
|
|
649
|
-
let escHovered = false;
|
|
650
|
-
let hoveredKey: PreviewKey | undefined;
|
|
651
|
-
|
|
652
|
-
return {
|
|
653
|
-
invalidate() {},
|
|
654
|
-
handleInput(data: string) {
|
|
655
|
-
if (
|
|
656
|
-
matchesKey(data, Key.escape) ||
|
|
657
|
-
matchesKey(data, Key.ctrl("c"))
|
|
658
|
-
) {
|
|
659
|
-
done(undefined);
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
if (matchesKey(data, Key.up) && visiblePreviews.length > 0) {
|
|
663
|
-
selectedPreviewIndex =
|
|
664
|
-
(selectedPreviewIndex - 1 + visiblePreviews.length) %
|
|
665
|
-
visiblePreviews.length;
|
|
666
|
-
tui.requestRender();
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
if (
|
|
670
|
-
matchesKey(data, Key.down) &&
|
|
671
|
-
visiblePreviews.length > 0
|
|
672
|
-
) {
|
|
673
|
-
selectedPreviewIndex =
|
|
674
|
-
(selectedPreviewIndex + 1) % visiblePreviews.length;
|
|
675
|
-
tui.requestRender();
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
if (matchesKey(data, Key.enter)) {
|
|
679
|
-
done(visiblePreviews[selectedPreviewIndex]?.key);
|
|
680
|
-
return;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
const mouse = parseSgrMousePacket(data);
|
|
684
|
-
if (!mouse || mouse.final !== "M") return;
|
|
685
|
-
if ((mouse.code & 32) !== 0) {
|
|
686
|
-
// SGR 1003 的无按键 hover code 为 35,不能按左键事件过滤。
|
|
687
|
-
const overEsc = Boolean(
|
|
688
|
-
escHitbox &&
|
|
689
|
-
mouse.row === escHitbox.row &&
|
|
690
|
-
mouse.col >= escHitbox.startCol &&
|
|
691
|
-
mouse.col <= escHitbox.endCol,
|
|
692
|
-
);
|
|
693
|
-
const hovered = previewHitboxes.find(
|
|
694
|
-
(candidate) =>
|
|
695
|
-
mouse.row === candidate.row &&
|
|
696
|
-
mouse.col >= candidate.startCol &&
|
|
697
|
-
mouse.col <= candidate.endCol,
|
|
698
|
-
);
|
|
699
|
-
if (overEsc !== escHovered || hovered?.key !== hoveredKey) {
|
|
700
|
-
escHovered = overEsc;
|
|
701
|
-
hoveredKey = hovered?.key;
|
|
702
|
-
tui.requestRender();
|
|
703
|
-
}
|
|
704
|
-
return;
|
|
705
|
-
}
|
|
706
|
-
if (mouseBaseButton(mouse.code) !== 0) return;
|
|
707
|
-
if (
|
|
708
|
-
escHitbox &&
|
|
709
|
-
mouse.row === escHitbox.row &&
|
|
710
|
-
mouse.col >= escHitbox.startCol &&
|
|
711
|
-
mouse.col <= escHitbox.endCol
|
|
712
|
-
) {
|
|
713
|
-
done(undefined);
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
const hitbox = previewHitboxes.find(
|
|
717
|
-
(candidate) =>
|
|
718
|
-
mouse.row === candidate.row &&
|
|
719
|
-
mouse.col >= candidate.startCol &&
|
|
720
|
-
mouse.col <= candidate.endCol,
|
|
721
|
-
);
|
|
722
|
-
if (hitbox) {
|
|
723
|
-
selectedPreviewIndex = Math.max(
|
|
724
|
-
0,
|
|
725
|
-
visiblePreviews.findIndex(
|
|
726
|
-
(preview) => preview.key === hitbox.key,
|
|
727
|
-
),
|
|
728
|
-
);
|
|
729
|
-
done(hitbox.key);
|
|
730
|
-
}
|
|
731
|
-
},
|
|
732
|
-
render(width: number) {
|
|
733
|
-
const inner = Math.max(1, width - 2);
|
|
734
|
-
const escWidth = visibleWidth("[esc]");
|
|
735
|
-
const percent =
|
|
736
|
-
contextWindow > 0 ? (used / contextWindow) * 100 : 0;
|
|
737
|
-
const title = theme.bold(theme.fg("accent", "Context Usage"));
|
|
738
|
-
const subtitle = `${formatTokens(used)} / ${formatTokens(contextWindow)} tokens (${percent.toFixed(1)}%)`;
|
|
739
|
-
const barWidth = Math.max(1, Math.min(60, inner - 2));
|
|
740
|
-
let remaining = barWidth;
|
|
741
|
-
const segments = allParts
|
|
742
|
-
.map((part, index) => {
|
|
743
|
-
const cells =
|
|
744
|
-
index === allParts.length - 1
|
|
745
|
-
? remaining
|
|
746
|
-
: Math.min(
|
|
747
|
-
remaining,
|
|
748
|
-
Math.round(
|
|
749
|
-
(part.tokens / Math.max(1, contextWindow)) *
|
|
750
|
-
barWidth,
|
|
751
|
-
),
|
|
752
|
-
);
|
|
753
|
-
remaining -= cells;
|
|
754
|
-
return theme.fg(
|
|
755
|
-
part.color,
|
|
756
|
-
"█".repeat(Math.max(0, cells)),
|
|
757
|
-
);
|
|
758
|
-
})
|
|
759
|
-
.join("");
|
|
760
|
-
const labelWidth = Math.min(
|
|
761
|
-
24,
|
|
762
|
-
Math.max(...allParts.map((part) => part.label.length)),
|
|
763
|
-
);
|
|
764
|
-
const selectedLabel =
|
|
765
|
-
visiblePreviews[selectedPreviewIndex]?.label;
|
|
766
|
-
const partRows = allParts.map((part) => {
|
|
767
|
-
const pct =
|
|
768
|
-
contextWindow > 0
|
|
769
|
-
? (part.tokens / contextWindow) * 100
|
|
770
|
-
: 0;
|
|
771
|
-
const swatch = theme.fg(part.color, "■");
|
|
772
|
-
const label = part.label.padEnd(labelWidth);
|
|
773
|
-
const amount = `${formatTokens(part.tokens).padStart(7)} ${pct.toFixed(1).padStart(5)}%`;
|
|
774
|
-
const selected = part.label === selectedLabel;
|
|
775
|
-
const hoverLabel = visiblePreviews.find(
|
|
776
|
-
(p) => p.key === hoveredKey,
|
|
777
|
-
)?.label;
|
|
778
|
-
const prefix = selected ? "› " : " ";
|
|
779
|
-
const row = padLine(
|
|
780
|
-
`${prefix}${swatch} ${label} ${amount}`,
|
|
781
|
-
inner,
|
|
782
|
-
);
|
|
783
|
-
if (selected) return theme.bg("selectedBg", row);
|
|
784
|
-
return part.label === hoverLabel
|
|
785
|
-
? theme.bg("customMessageBg", row)
|
|
786
|
-
: row;
|
|
787
|
-
});
|
|
788
|
-
const border = (text: string) => theme.fg("border", text);
|
|
789
|
-
const lines = [
|
|
790
|
-
border(`╭${"─".repeat(inner)}╮`),
|
|
791
|
-
`${border("│")}${padLine(` ${title} ${theme.fg("muted", subtitle)}`, inner - escWidth)}${theme.fg(escHovered ? "text" : "muted", "[esc]")}${border("│")}`,
|
|
792
|
-
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
793
|
-
`${border("│")}${padLine(` ${segments}`, inner)}${border("│")}`,
|
|
794
|
-
`${border("│")}${" ".repeat(inner)}${border("│")}`,
|
|
795
|
-
...partRows.map(
|
|
796
|
-
(row) => `${border("│")}${row}${border("│")}`,
|
|
797
|
-
),
|
|
798
|
-
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
799
|
-
`${border("│")}${padLine(theme.fg("dim", " ↑↓ select · Click / Enter to preview · [esc] close"), inner)}${border("│")}`,
|
|
800
|
-
border(`╰${"─".repeat(inner)}╯`),
|
|
801
|
-
];
|
|
802
|
-
|
|
803
|
-
const terminalHeight = Math.max(1, tui.terminal.rows);
|
|
804
|
-
const maxHeight = Math.min(
|
|
805
|
-
Math.max(1, Math.floor(terminalHeight * 0.9)),
|
|
806
|
-
Math.max(1, terminalHeight - 2),
|
|
807
|
-
);
|
|
808
|
-
const visibleHeight = Math.min(lines.length, maxHeight);
|
|
809
|
-
const overlayTop =
|
|
810
|
-
1 +
|
|
811
|
-
Math.floor(
|
|
812
|
-
(Math.max(1, terminalHeight - 2) - visibleHeight) / 2,
|
|
813
|
-
);
|
|
814
|
-
const overlayLeft = Math.floor(
|
|
815
|
-
(Math.max(1, tui.terminal.columns) - width) / 2,
|
|
816
|
-
);
|
|
817
|
-
escHitbox = escCloseHitbox({
|
|
818
|
-
left: overlayLeft,
|
|
819
|
-
top: overlayTop,
|
|
820
|
-
width,
|
|
821
|
-
});
|
|
822
|
-
previewHitboxes = visiblePreviews.flatMap((preview) => {
|
|
823
|
-
const partIndex = allParts.findIndex(
|
|
824
|
-
(part) => part.label === preview.label,
|
|
825
|
-
);
|
|
826
|
-
const line = 5 + partIndex;
|
|
827
|
-
return partIndex >= 0 && line < visibleHeight
|
|
828
|
-
? [
|
|
829
|
-
{
|
|
830
|
-
key: preview.key,
|
|
831
|
-
row: overlayTop + line + 1,
|
|
832
|
-
startCol: overlayLeft + 1,
|
|
833
|
-
endCol: overlayLeft + width,
|
|
834
|
-
},
|
|
835
|
-
]
|
|
836
|
-
: [];
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
return lines;
|
|
840
|
-
},
|
|
841
|
-
};
|
|
842
|
-
},
|
|
843
|
-
{
|
|
844
|
-
overlay: true,
|
|
845
|
-
overlayOptions: {
|
|
846
|
-
anchor: "center",
|
|
847
|
-
width: 64,
|
|
848
|
-
minWidth: 44,
|
|
849
|
-
maxHeight: "90%",
|
|
850
|
-
margin: 1,
|
|
851
|
-
},
|
|
852
|
-
},
|
|
853
|
-
),
|
|
269
|
+
if (ctx.mode !== "tui") {
|
|
270
|
+
ctx.ui.notify(
|
|
271
|
+
allParts
|
|
272
|
+
.map((part) => `${part.label}: ${formatTokens(part.tokens)} tokens`)
|
|
273
|
+
.join("\n"),
|
|
274
|
+
"info",
|
|
854
275
|
);
|
|
855
|
-
|
|
856
|
-
if (!action) break;
|
|
857
|
-
const preview = previewByKey.get(action as PreviewKey);
|
|
858
|
-
if (!preview) continue;
|
|
859
|
-
|
|
860
|
-
await showTextPreview(ctx, preview.title, preview.content);
|
|
276
|
+
return;
|
|
861
277
|
}
|
|
278
|
+
await showContextInspector(
|
|
279
|
+
ctx,
|
|
280
|
+
{ parts: allParts, used, contextWindow, previews: breakdown.previews },
|
|
281
|
+
overlays,
|
|
282
|
+
);
|
|
862
283
|
},
|
|
863
284
|
});
|
|
864
285
|
}
|