pi-cc-extensions 0.7.3 → 0.7.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.md +1 -1
- package/extensions/agent-autocomplete.ts +1 -1
- package/extensions/claude-code-style.ts +1713 -1675
- package/extensions/context.ts +386 -362
- package/extensions/session-reference/README.md +2 -2
- package/extensions/session-reference/index.ts +32 -5
- package/package.json +1 -1
package/extensions/context.ts
CHANGED
|
@@ -1,362 +1,386 @@
|
|
|
1
|
-
import { type ExtensionAPI, type ExtensionCommandContext, estimateTokens } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
3
|
-
|
|
4
|
-
export type ContextPart = {
|
|
5
|
-
label: string;
|
|
6
|
-
tokens: number;
|
|
7
|
-
color: "accent" | "success" | "warning" | "muted" | "dim";
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
type PreviewKey = "systemPrompt" | "tools" | "contextFiles" | "skills";
|
|
11
|
-
|
|
12
|
-
type ContextPreview = {
|
|
13
|
-
key: PreviewKey;
|
|
14
|
-
label: string;
|
|
15
|
-
title: string;
|
|
16
|
-
content: string;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
function normalizePreviewText(text: string): string {
|
|
20
|
-
return text
|
|
21
|
-
.replace(/\r\n?/g, "\n")
|
|
22
|
-
.replace(/\t/g, " ")
|
|
23
|
-
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
.map((
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
1
|
+
import { type ExtensionAPI, type ExtensionCommandContext, estimateTokens } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
export type ContextPart = {
|
|
5
|
+
label: string;
|
|
6
|
+
tokens: number;
|
|
7
|
+
color: "accent" | "success" | "warning" | "muted" | "dim";
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type PreviewKey = "systemPrompt" | "tools" | "contextFiles" | "skills";
|
|
11
|
+
|
|
12
|
+
type ContextPreview = {
|
|
13
|
+
key: PreviewKey;
|
|
14
|
+
label: string;
|
|
15
|
+
title: string;
|
|
16
|
+
content: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function normalizePreviewText(text: string): string {
|
|
20
|
+
return text
|
|
21
|
+
.replace(/\r\n?/g, "\n")
|
|
22
|
+
.replace(/\t/g, " ")
|
|
23
|
+
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, "");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createWrappedTextCache(content: string): {
|
|
27
|
+
get(width: number): string[];
|
|
28
|
+
invalidate(): void;
|
|
29
|
+
} {
|
|
30
|
+
let cachedWidth: number | undefined;
|
|
31
|
+
let cachedLines: string[] | undefined;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
get(width: number): string[] {
|
|
35
|
+
if (cachedLines !== undefined && cachedWidth === width) return cachedLines;
|
|
36
|
+
cachedLines = wrapTextWithAnsi(content, width);
|
|
37
|
+
cachedWidth = width;
|
|
38
|
+
return cachedLines;
|
|
39
|
+
},
|
|
40
|
+
invalidate(): void {
|
|
41
|
+
cachedWidth = undefined;
|
|
42
|
+
cachedLines = undefined;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function showTextPreview(ctx: Pick<ExtensionCommandContext, "ui">, title: string, rawContent: string): Promise<void> {
|
|
48
|
+
const content = normalizePreviewText(rawContent);
|
|
49
|
+
await ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
|
|
50
|
+
let scrollOffset = 0;
|
|
51
|
+
let pageSize = 1;
|
|
52
|
+
let totalLines = 1;
|
|
53
|
+
const wrappedContent = createWrappedTextCache(content);
|
|
54
|
+
|
|
55
|
+
const scrollTo = (nextOffset: number): void => {
|
|
56
|
+
scrollOffset = Math.max(0, Math.min(nextOffset, Math.max(0, totalLines - pageSize)));
|
|
57
|
+
tui.requestRender();
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
invalidate() {
|
|
62
|
+
wrappedContent.invalidate();
|
|
63
|
+
},
|
|
64
|
+
handleInput(data: string) {
|
|
65
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
|
|
66
|
+
done();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (matchesKey(data, Key.up)) scrollTo(scrollOffset - 1);
|
|
70
|
+
else if (matchesKey(data, Key.down)) scrollTo(scrollOffset + 1);
|
|
71
|
+
else if (matchesKey(data, "pageUp")) scrollTo(scrollOffset - pageSize);
|
|
72
|
+
else if (matchesKey(data, "pageDown")) scrollTo(scrollOffset + pageSize);
|
|
73
|
+
else if (matchesKey(data, Key.home)) scrollTo(0);
|
|
74
|
+
else if (matchesKey(data, Key.end)) scrollTo(totalLines - pageSize);
|
|
75
|
+
else {
|
|
76
|
+
const mouse = parseSgrMousePacket(data);
|
|
77
|
+
if (mouse?.final !== "M") return;
|
|
78
|
+
const button = mouseBaseButton(mouse.code);
|
|
79
|
+
if (button === 64) scrollTo(scrollOffset - 3);
|
|
80
|
+
else if (button === 65) scrollTo(scrollOffset + 3);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
render(width: number) {
|
|
84
|
+
const inner = Math.max(1, width - 2);
|
|
85
|
+
const bodyInner = Math.max(1, inner - 1);
|
|
86
|
+
const bodyWidth = Math.max(1, bodyInner - 1);
|
|
87
|
+
const terminalHeight = Math.max(1, tui.terminal.rows);
|
|
88
|
+
const availableHeight = Math.max(1, terminalHeight - 4);
|
|
89
|
+
const viewportHeight = Math.min(30, Math.max(1, Math.floor(terminalHeight * 0.8)), availableHeight);
|
|
90
|
+
pageSize = Math.max(1, viewportHeight - 6);
|
|
91
|
+
const wrapped = wrappedContent.get(bodyWidth);
|
|
92
|
+
totalLines = wrapped.length;
|
|
93
|
+
scrollOffset = Math.min(scrollOffset, Math.max(0, totalLines - pageSize));
|
|
94
|
+
const visible = wrapped.slice(scrollOffset, scrollOffset + pageSize);
|
|
95
|
+
const border = (text: string) => theme.fg("border", text);
|
|
96
|
+
const padLine = (text: string, lineWidth = inner): string => {
|
|
97
|
+
const truncated = truncateToWidth(text, lineWidth, "…");
|
|
98
|
+
return truncated + " ".repeat(Math.max(0, lineWidth - visibleWidth(truncated)));
|
|
99
|
+
};
|
|
100
|
+
const scrollable = totalLines > pageSize;
|
|
101
|
+
const thumbSize = scrollable ? Math.max(1, Math.floor((pageSize * pageSize) / totalLines)) : 0;
|
|
102
|
+
const maxScrollOffset = Math.max(0, totalLines - pageSize);
|
|
103
|
+
const thumbStart = scrollable && maxScrollOffset > 0
|
|
104
|
+
? Math.round((scrollOffset / maxScrollOffset) * (pageSize - thumbSize))
|
|
105
|
+
: 0;
|
|
106
|
+
const scrollbar = (row: number): string => {
|
|
107
|
+
if (!scrollable) return " ";
|
|
108
|
+
const inThumb = row >= thumbStart && row < thumbStart + thumbSize;
|
|
109
|
+
return theme.fg(inThumb ? "accent" : "borderMuted", inThumb ? "█" : "│");
|
|
110
|
+
};
|
|
111
|
+
const bodyRows = Array.from({ length: pageSize }, (_, row) => {
|
|
112
|
+
const line = visible[row] ?? "";
|
|
113
|
+
return `${border("│")}${padLine(` ${line}`, bodyInner)}${scrollbar(row)}${border("│")}`;
|
|
114
|
+
});
|
|
115
|
+
const start = totalLines === 0 ? 0 : scrollOffset + 1;
|
|
116
|
+
const end = Math.min(totalLines, scrollOffset + pageSize);
|
|
117
|
+
const status = `${start}-${end} / ${totalLines} lines · ↑↓ PgUp/PgDn Home/End · Esc close`;
|
|
118
|
+
|
|
119
|
+
return [
|
|
120
|
+
border(`╭${"─".repeat(inner)}╮`),
|
|
121
|
+
`${border("│")}${padLine(` ${theme.bold(theme.fg("accent", title))}`)}${border("│")}`,
|
|
122
|
+
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
123
|
+
...bodyRows,
|
|
124
|
+
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
125
|
+
`${border("│")}${padLine(theme.fg("dim", ` ${status}`))}${border("│")}`,
|
|
126
|
+
border(`╰${"─".repeat(inner)}╯`),
|
|
127
|
+
];
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}, {
|
|
131
|
+
overlay: true,
|
|
132
|
+
overlayOptions: { anchor: "center", width: "85%", minWidth: 50, maxHeight: "80%", margin: 2 },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
type SgrMousePacket = {
|
|
137
|
+
code: number;
|
|
138
|
+
col: number;
|
|
139
|
+
row: number;
|
|
140
|
+
final: "M" | "m";
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
function parseSgrMousePacket(data: string): SgrMousePacket | null {
|
|
144
|
+
const match = data.match(/^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/);
|
|
145
|
+
if (!match) return null;
|
|
146
|
+
return {
|
|
147
|
+
code: Number(match[1]),
|
|
148
|
+
col: Number(match[2]),
|
|
149
|
+
row: Number(match[3]),
|
|
150
|
+
final: match[4] as "M" | "m",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function mouseBaseButton(code: number): number {
|
|
155
|
+
return code & ~(4 | 8 | 16 | 32);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const tokenEstimate = (value: unknown): number => {
|
|
159
|
+
if (!value) return 0;
|
|
160
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
161
|
+
return Math.max(0, Math.ceil(text.length / 4));
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export function scaleParts(parts: ContextPart[], target: number): ContextPart[] {
|
|
165
|
+
const estimated = parts.reduce((sum, part) => sum + part.tokens, 0);
|
|
166
|
+
if (estimated === 0 || target <= 0) return parts;
|
|
167
|
+
const scaled = parts.map((part) => ({ ...part, tokens: Math.round((part.tokens / estimated) * target) }));
|
|
168
|
+
const delta = target - scaled.reduce((sum, part) => sum + part.tokens, 0);
|
|
169
|
+
const largest = scaled.reduce((best, part, index) => part.tokens > scaled[best]!.tokens ? index : best, 0);
|
|
170
|
+
scaled[largest]!.tokens += delta;
|
|
171
|
+
return scaled;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function formatTokens(tokens: number): string {
|
|
175
|
+
if (tokens < 1_000) return String(tokens);
|
|
176
|
+
if (tokens < 100_000) return `${(tokens / 1_000).toFixed(1)}k`;
|
|
177
|
+
return `${Math.round(tokens / 1_000)}k`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function collectParts(ctx: ExtensionCommandContext): ContextPart[] {
|
|
181
|
+
const options = ctx.getSystemPromptOptions();
|
|
182
|
+
const contextFileTokens = (options.contextFiles ?? []).reduce((sum, file) => sum + tokenEstimate(file.content), 0);
|
|
183
|
+
const skillTokens = (options.skills ?? []).reduce((sum, skill) => sum + tokenEstimate(skill), 0);
|
|
184
|
+
const tools = options.selectedTools ?? [];
|
|
185
|
+
const toolTokens = tools.reduce((sum, name) => sum + tokenEstimate(name) + tokenEstimate(options.toolSnippets?.[name]), 0)
|
|
186
|
+
+ tokenEstimate(options.promptGuidelines);
|
|
187
|
+
|
|
188
|
+
let user = 0;
|
|
189
|
+
let assistant = 0;
|
|
190
|
+
let toolResults = 0;
|
|
191
|
+
let summaries = 0;
|
|
192
|
+
for (const entry of ctx.sessionManager.buildContextEntries()) {
|
|
193
|
+
if (entry.type === "message") {
|
|
194
|
+
const tokens = estimateTokens(entry.message);
|
|
195
|
+
if (entry.message.role === "user") user += tokens;
|
|
196
|
+
else if (entry.message.role === "assistant") assistant += tokens;
|
|
197
|
+
else toolResults += tokens;
|
|
198
|
+
} else if (entry.type === "compaction" || entry.type === "branch_summary") {
|
|
199
|
+
summaries += tokenEstimate(entry);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const systemTotal = tokenEstimate(ctx.getSystemPrompt());
|
|
204
|
+
const baseSystem = Math.max(0, systemTotal - contextFileTokens - skillTokens - toolTokens);
|
|
205
|
+
const parts: ContextPart[] = [
|
|
206
|
+
{ label: "System prompt", tokens: baseSystem, color: "accent" },
|
|
207
|
+
{ label: "Tools", tokens: toolTokens, color: "success" },
|
|
208
|
+
{ label: "Context files", tokens: contextFileTokens, color: "warning" },
|
|
209
|
+
{ label: "Skills", tokens: skillTokens, color: "warning" },
|
|
210
|
+
{ label: "User messages", tokens: user, color: "muted" },
|
|
211
|
+
{ label: "Assistant messages", tokens: assistant, color: "accent" },
|
|
212
|
+
{ label: "Tool results", tokens: toolResults, color: "dim" },
|
|
213
|
+
{ label: "Compaction summaries", tokens: summaries, color: "success" },
|
|
214
|
+
];
|
|
215
|
+
return parts.filter((part) => part.tokens > 0);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export default function contextUsageExtension(pi: ExtensionAPI) {
|
|
219
|
+
pi.registerCommand("context", {
|
|
220
|
+
description: "Show the current context-window distribution",
|
|
221
|
+
handler: async (_args, ctx) => {
|
|
222
|
+
const usage = ctx.getContextUsage();
|
|
223
|
+
const contextWindow = usage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
|
|
224
|
+
const used = usage?.tokens ?? collectParts(ctx).reduce((sum, part) => sum + part.tokens, 0);
|
|
225
|
+
const parts = scaleParts(collectParts(ctx), used);
|
|
226
|
+
const free = Math.max(0, contextWindow - used);
|
|
227
|
+
const allParts = [...parts, { label: "Free space", tokens: free, color: "dim" as const }];
|
|
228
|
+
|
|
229
|
+
if (ctx.mode !== "tui") {
|
|
230
|
+
const lines = allParts.map((part) => `${part.label}: ${formatTokens(part.tokens)} tokens`);
|
|
231
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const options = ctx.getSystemPromptOptions();
|
|
236
|
+
const toolByName = new Map(pi.getAllTools().map((tool) => [tool.name, tool]));
|
|
237
|
+
const toolContent = (options.selectedTools ?? []).map((name) => {
|
|
238
|
+
const tool = toolByName.get(name);
|
|
239
|
+
const lines = [`## ${name}`];
|
|
240
|
+
if (tool?.description) lines.push(tool.description);
|
|
241
|
+
if (options.toolSnippets?.[name]) lines.push(`Prompt: ${options.toolSnippets[name]}`);
|
|
242
|
+
if (tool?.promptGuidelines?.length) {
|
|
243
|
+
lines.push("Guidelines:", ...tool.promptGuidelines.map((guideline) => `- ${guideline}`));
|
|
244
|
+
}
|
|
245
|
+
return lines.join("\n");
|
|
246
|
+
});
|
|
247
|
+
if (options.promptGuidelines?.length) {
|
|
248
|
+
toolContent.push(`## Shared prompt guidelines\n${options.promptGuidelines.map((guideline) => `- ${guideline}`).join("\n")}`);
|
|
249
|
+
}
|
|
250
|
+
const contextFilesContent = (options.contextFiles ?? [])
|
|
251
|
+
.map((file) => `===== ${file.path} =====\n${file.content}`)
|
|
252
|
+
.join("\n\n");
|
|
253
|
+
const skillsContent = (options.skills ?? [])
|
|
254
|
+
.map((skill) => [
|
|
255
|
+
`## ${skill.name}`,
|
|
256
|
+
skill.description,
|
|
257
|
+
`Path: ${skill.filePath}`,
|
|
258
|
+
`Model invocation: ${skill.disableModelInvocation ? "disabled" : "enabled"}`,
|
|
259
|
+
].filter(Boolean).join("\n"))
|
|
260
|
+
.join("\n\n");
|
|
261
|
+
const rawPreviews: ContextPreview[] = [
|
|
262
|
+
{ key: "systemPrompt", label: "System prompt", title: "System Prompt", content: ctx.getSystemPrompt() },
|
|
263
|
+
{ key: "tools", label: "Tools", title: "Tools", content: toolContent.join("\n\n") || "No active tools." },
|
|
264
|
+
{ key: "contextFiles", label: "Context files", title: "Context Files", content: contextFilesContent || "No context files loaded." },
|
|
265
|
+
{ key: "skills", label: "Skills", title: "Skills", content: skillsContent || "No skills loaded." },
|
|
266
|
+
];
|
|
267
|
+
const previews = rawPreviews.map((preview) => ({
|
|
268
|
+
...preview,
|
|
269
|
+
content: normalizePreviewText(preview.content),
|
|
270
|
+
}));
|
|
271
|
+
const previewByKey = new Map(previews.map((preview) => [preview.key, preview]));
|
|
272
|
+
const visiblePreviews = previews.filter((preview) => allParts.some((part) => part.label === preview.label));
|
|
273
|
+
let selectedPreviewIndex = 0;
|
|
274
|
+
|
|
275
|
+
while (true) {
|
|
276
|
+
const action = await ctx.ui.custom<PreviewKey | undefined>((tui, theme, _keybindings, done) => {
|
|
277
|
+
let previewHitboxes: Array<{ key: PreviewKey; row: number; startCol: number; endCol: number }> = [];
|
|
278
|
+
|
|
279
|
+
const padLine = (text: string, width: number): string => {
|
|
280
|
+
const truncated = truncateToWidth(text, width, "…");
|
|
281
|
+
return truncated + " ".repeat(Math.max(0, width - visibleWidth(truncated)));
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
invalidate() {},
|
|
286
|
+
handleInput(data: string) {
|
|
287
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
|
|
288
|
+
done(undefined);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (matchesKey(data, Key.up) && visiblePreviews.length > 0) {
|
|
292
|
+
selectedPreviewIndex = (selectedPreviewIndex - 1 + visiblePreviews.length) % visiblePreviews.length;
|
|
293
|
+
tui.requestRender();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (matchesKey(data, Key.down) && visiblePreviews.length > 0) {
|
|
297
|
+
selectedPreviewIndex = (selectedPreviewIndex + 1) % visiblePreviews.length;
|
|
298
|
+
tui.requestRender();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (matchesKey(data, Key.enter)) {
|
|
302
|
+
done(visiblePreviews[selectedPreviewIndex]?.key);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const mouse = parseSgrMousePacket(data);
|
|
307
|
+
if (mouse?.final !== "M" || mouseBaseButton(mouse.code) !== 0 || (mouse.code & 32) !== 0) return;
|
|
308
|
+
const hitbox = previewHitboxes.find((candidate) =>
|
|
309
|
+
mouse.row === candidate.row
|
|
310
|
+
&& mouse.col >= candidate.startCol
|
|
311
|
+
&& mouse.col <= candidate.endCol
|
|
312
|
+
);
|
|
313
|
+
if (hitbox) {
|
|
314
|
+
selectedPreviewIndex = Math.max(0, visiblePreviews.findIndex((preview) => preview.key === hitbox.key));
|
|
315
|
+
done(hitbox.key);
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
render(width: number) {
|
|
319
|
+
const inner = Math.max(1, width - 2);
|
|
320
|
+
const percent = contextWindow > 0 ? (used / contextWindow) * 100 : 0;
|
|
321
|
+
const title = theme.bold(theme.fg("accent", "Context Usage"));
|
|
322
|
+
const subtitle = `${formatTokens(used)} / ${formatTokens(contextWindow)} tokens (${percent.toFixed(1)}%)`;
|
|
323
|
+
const barWidth = Math.max(1, Math.min(60, inner - 2));
|
|
324
|
+
let remaining = barWidth;
|
|
325
|
+
const segments = allParts.map((part, index) => {
|
|
326
|
+
const cells = index === allParts.length - 1
|
|
327
|
+
? remaining
|
|
328
|
+
: Math.min(remaining, Math.round((part.tokens / Math.max(1, contextWindow)) * barWidth));
|
|
329
|
+
remaining -= cells;
|
|
330
|
+
return theme.fg(part.color, "█".repeat(Math.max(0, cells)));
|
|
331
|
+
}).join("");
|
|
332
|
+
const labelWidth = Math.min(24, Math.max(...allParts.map((part) => part.label.length)));
|
|
333
|
+
const selectedLabel = visiblePreviews[selectedPreviewIndex]?.label;
|
|
334
|
+
const partRows = allParts.map((part) => {
|
|
335
|
+
const pct = contextWindow > 0 ? (part.tokens / contextWindow) * 100 : 0;
|
|
336
|
+
const swatch = theme.fg(part.color, "■");
|
|
337
|
+
const label = part.label.padEnd(labelWidth);
|
|
338
|
+
const amount = `${formatTokens(part.tokens).padStart(7)} ${pct.toFixed(1).padStart(5)}%`;
|
|
339
|
+
const selected = part.label === selectedLabel;
|
|
340
|
+
const prefix = selected ? "› " : " ";
|
|
341
|
+
const row = padLine(`${prefix}${swatch} ${label} ${amount}`, inner);
|
|
342
|
+
return selected ? theme.bg("selectedBg", row) : row;
|
|
343
|
+
});
|
|
344
|
+
const border = (text: string) => theme.fg("border", text);
|
|
345
|
+
const lines = [
|
|
346
|
+
border(`╭${"─".repeat(inner)}╮`),
|
|
347
|
+
`${border("│")}${padLine(` ${title} ${theme.fg("muted", subtitle)}`, inner)}${border("│")}`,
|
|
348
|
+
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
349
|
+
`${border("│")}${padLine(` ${segments}`, inner)}${border("│")}`,
|
|
350
|
+
`${border("│")}${" ".repeat(inner)}${border("│")}`,
|
|
351
|
+
...partRows.map((row) => `${border("│")}${row}${border("│")}`),
|
|
352
|
+
`${border("├")}${border("─".repeat(inner))}${border("┤")}`,
|
|
353
|
+
`${border("│")}${padLine(theme.fg("dim", " ↑↓ select · Click / Enter to preview · Esc to close"), inner)}${border("│")}`,
|
|
354
|
+
border(`╰${"─".repeat(inner)}╯`),
|
|
355
|
+
];
|
|
356
|
+
|
|
357
|
+
const terminalHeight = Math.max(1, tui.terminal.rows);
|
|
358
|
+
const maxHeight = Math.min(Math.max(1, Math.floor(terminalHeight * 0.9)), Math.max(1, terminalHeight - 2));
|
|
359
|
+
const visibleHeight = Math.min(lines.length, maxHeight);
|
|
360
|
+
const overlayTop = 1 + Math.floor((Math.max(1, terminalHeight - 2) - visibleHeight) / 2);
|
|
361
|
+
const overlayLeft = Math.floor((Math.max(1, tui.terminal.columns) - width) / 2);
|
|
362
|
+
previewHitboxes = visiblePreviews.flatMap((preview) => {
|
|
363
|
+
const partIndex = allParts.findIndex((part) => part.label === preview.label);
|
|
364
|
+
const line = 5 + partIndex;
|
|
365
|
+
return partIndex >= 0 && line < visibleHeight
|
|
366
|
+
? [{ key: preview.key, row: overlayTop + line + 1, startCol: overlayLeft + 1, endCol: overlayLeft + width }]
|
|
367
|
+
: [];
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
return lines;
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
}, {
|
|
374
|
+
overlay: true,
|
|
375
|
+
overlayOptions: { anchor: "center", width: 64, minWidth: 44, maxHeight: "90%", margin: 1 },
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
if (!action) break;
|
|
379
|
+
const preview = previewByKey.get(action);
|
|
380
|
+
if (!preview) continue;
|
|
381
|
+
|
|
382
|
+
await showTextPreview(ctx, preview.title, preview.content);
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
}
|