@mrclrchtr/supi-context 1.3.1 → 1.5.0
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 +39 -24
- package/node_modules/@mrclrchtr/supi-core/README.md +52 -41
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +15 -13
- package/node_modules/@mrclrchtr/supi-core/src/{config-settings.ts → config/config-settings.ts} +2 -2
- package/node_modules/@mrclrchtr/supi-core/src/{context-provider-registry.ts → context/context-provider-registry.ts} +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/extension.ts +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +15 -13
- package/node_modules/@mrclrchtr/supi-core/src/path-utils.ts +40 -0
- package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +42 -10
- package/node_modules/@mrclrchtr/supi-core/src/{settings-registry.ts → settings/settings-registry.ts} +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +223 -19
- package/src/context.ts +4 -3
- package/src/format.ts +527 -116
- package/src/prompt-inference.ts +37 -13
- package/src/renderer.ts +30 -13
- /package/node_modules/@mrclrchtr/supi-core/src/{config.ts → config/config.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{context-messages.ts → context/context-messages.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{context-tag.ts → context/context-tag.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{settings-command.ts → settings/settings-command.ts} +0 -0
- /package/node_modules/@mrclrchtr/supi-core/src/{settings-ui.ts → settings/settings-ui.ts} +0 -0
package/src/format.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
+
// biome-ignore-all lint/nursery/noExcessiveLinesPerFile: format file is inherently large
|
|
2
|
+
|
|
1
3
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
5
|
import type { ContextAnalysis } from "./analysis.ts";
|
|
3
6
|
import { formatTokens, pluralize } from "./utils.ts";
|
|
4
7
|
|
|
5
|
-
const GRID_COLS = 20;
|
|
6
|
-
const GRID_ROWS = 5;
|
|
7
|
-
const GRID_BLOCKS = GRID_COLS * GRID_ROWS;
|
|
8
|
-
|
|
9
8
|
type CategoryKey =
|
|
10
9
|
| "systemPrompt"
|
|
11
10
|
| "userMessages"
|
|
@@ -54,227 +53,639 @@ function padRight(text: string, width: number): string {
|
|
|
54
53
|
return text.padEnd(width, " ");
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
function sum(values: number[]): number {
|
|
57
|
+
return values.reduce((total, value) => total + value, 0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function allocateBlocks(values: number[], totalBlocks: number): number[] {
|
|
61
|
+
const total = sum(values);
|
|
62
|
+
if (total <= 0 || totalBlocks <= 0) {
|
|
63
|
+
return values.map(() => 0);
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
const exact =
|
|
66
|
+
const exact = values.map((value) => (value / total) * totalBlocks);
|
|
64
67
|
const counts = exact.map((value) => Math.floor(value));
|
|
65
|
-
const remaining =
|
|
68
|
+
const remaining = totalBlocks - sum(counts);
|
|
66
69
|
|
|
67
70
|
const byRemainder = exact
|
|
68
71
|
.map((value, index) => ({ index, remainder: value - counts[index] }))
|
|
69
72
|
.sort((a, b) => b.remainder - a.remainder);
|
|
70
73
|
|
|
71
|
-
for (let i = 0; i < remaining; i
|
|
74
|
+
for (let i = 0; i < remaining; i += 1) {
|
|
72
75
|
counts[byRemainder[i]?.index ?? 0] += 1;
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
return counts;
|
|
76
79
|
}
|
|
77
80
|
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
function healthColor(analysis: ContextAnalysis): Parameters<Theme["fg"]>["0"] {
|
|
82
|
+
if (analysis.contextWindow <= 0) return "dim";
|
|
83
|
+
const reserved = analysis.totalTokens ?? 0;
|
|
84
|
+
const pressure =
|
|
85
|
+
((reserved + analysis.categories.autocompactBuffer) / analysis.contextWindow) * 100;
|
|
86
|
+
if (pressure >= 90) return "error";
|
|
87
|
+
if (pressure >= 70) return "warning";
|
|
88
|
+
return "success";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function sectionHeader(title: string, meta: string | null, theme: Theme, width: number): string {
|
|
92
|
+
const left = theme.fg("text", title);
|
|
93
|
+
const content = meta ? `${left}${theme.fg("dim", ` ${meta}`)}` : left;
|
|
94
|
+
return truncateToWidth(content, width);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function renderSummary(analysis: ContextAnalysis, theme: Theme, width: number): string[] {
|
|
98
|
+
const used = analysis.totalTokens ?? 0;
|
|
99
|
+
const health = theme.fg(healthColor(analysis), "●");
|
|
100
|
+
const usage =
|
|
101
|
+
analysis.contextWindow > 0
|
|
102
|
+
? `${formatTokens(used)} / ${formatTokens(analysis.contextWindow)} tokens (${pct(used, analysis.contextWindow)})`
|
|
103
|
+
: `${formatTokens(used)} tokens`;
|
|
104
|
+
|
|
105
|
+
const lines = [
|
|
106
|
+
truncateToWidth(
|
|
107
|
+
`${health} ${theme.fg("text", analysis.modelName)}${theme.fg("dim", ` · ${usage}`)}`,
|
|
108
|
+
width,
|
|
109
|
+
),
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
if (analysis.approximationNote) {
|
|
113
|
+
lines.push(...wrapTextWithAnsi(theme.fg("warning", analysis.approximationNote), width));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return lines;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function renderUsageBar(analysis: ContextAnalysis, theme: Theme, width: number): string[] {
|
|
120
|
+
if (analysis.contextWindow <= 0) {
|
|
121
|
+
return [theme.fg("dim", "No model selected — usage bar unavailable")];
|
|
82
122
|
}
|
|
83
123
|
|
|
124
|
+
const percentLabel = pct(analysis.totalTokens ?? 0, analysis.contextWindow);
|
|
125
|
+
const barWidth = Math.max(12, Math.min(48, width - visibleWidth(percentLabel) - 3));
|
|
126
|
+
const values = [
|
|
127
|
+
...CATEGORY_ORDER.map((key) => analysis.categories[key]),
|
|
128
|
+
analysis.categories.autocompactBuffer,
|
|
129
|
+
analysis.categories.freeSpace,
|
|
130
|
+
];
|
|
131
|
+
const counts = allocateBlocks(values, barWidth);
|
|
132
|
+
|
|
84
133
|
const segments = [
|
|
85
|
-
...CATEGORY_ORDER.map((key) => ({
|
|
134
|
+
...CATEGORY_ORDER.map((key, index) => ({
|
|
86
135
|
color: CATEGORY_COLORS[key],
|
|
87
|
-
tokens: categories[key],
|
|
88
136
|
block: "█",
|
|
137
|
+
count: counts[index] ?? 0,
|
|
89
138
|
})),
|
|
90
|
-
{ color: "dim" as Parameters<Theme["fg"]>["0"], tokens: categories.freeSpace, block: "░" },
|
|
91
139
|
{
|
|
92
140
|
color: "warning" as Parameters<Theme["fg"]>["0"],
|
|
93
|
-
|
|
141
|
+
block: "▒",
|
|
142
|
+
count: counts[CATEGORY_ORDER.length] ?? 0,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
color: "dim" as Parameters<Theme["fg"]>["0"],
|
|
94
146
|
block: "░",
|
|
147
|
+
count: counts[CATEGORY_ORDER.length + 1] ?? 0,
|
|
95
148
|
},
|
|
96
149
|
];
|
|
97
150
|
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const gridLines: string[] = [];
|
|
107
|
-
for (let row = 0; row < GRID_ROWS; row++) {
|
|
108
|
-
const start = row * GRID_COLS;
|
|
109
|
-
const line = blocks.slice(start, start + GRID_COLS).join("");
|
|
110
|
-
gridLines.push(line);
|
|
111
|
-
}
|
|
151
|
+
const bar = segments
|
|
152
|
+
.map((segment) =>
|
|
153
|
+
segment.count > 0 ? theme.fg(segment.color, segment.block.repeat(segment.count)) : "",
|
|
154
|
+
)
|
|
155
|
+
.join("");
|
|
112
156
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
theme.fg(
|
|
118
|
-
"text",
|
|
119
|
-
`${formatTokens(analysis.totalTokens ?? 0)} used (${pct(analysis.totalTokens ?? 0, contextWindow)})`,
|
|
120
|
-
),
|
|
121
|
-
];
|
|
157
|
+
const barLine = truncateToWidth(
|
|
158
|
+
`${theme.fg("dim", "[")}${bar}${theme.fg("dim", "]")} ${theme.fg("text", percentLabel)}`,
|
|
159
|
+
width,
|
|
160
|
+
);
|
|
122
161
|
|
|
123
|
-
|
|
124
|
-
|
|
162
|
+
const legendParts: string[] = [];
|
|
163
|
+
for (const key of CATEGORY_ORDER) {
|
|
164
|
+
if (analysis.categories[key] <= 0) continue;
|
|
165
|
+
legendParts.push(`${theme.fg(CATEGORY_COLORS[key], "●")} ${CATEGORY_LABELS[key]}`);
|
|
125
166
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
for (let i = 0; i < GRID_ROWS; i++) {
|
|
129
|
-
const left = gridLines[i] ?? "";
|
|
130
|
-
const right = infoLines[i] ?? "";
|
|
131
|
-
combined.push(`${left} ${right}`);
|
|
167
|
+
if (analysis.categories.autocompactBuffer > 0) {
|
|
168
|
+
legendParts.push(`${theme.fg("warning", "▒")} Autocompact buffer`);
|
|
132
169
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
combined.push(`${" ".repeat(GRID_COLS + 2)}${infoLines[i]}`);
|
|
170
|
+
if (analysis.categories.freeSpace > 0) {
|
|
171
|
+
legendParts.push(`${theme.fg("dim", "░")} Free space`);
|
|
136
172
|
}
|
|
137
173
|
|
|
138
|
-
return
|
|
174
|
+
return [barLine, ...wrapTextWithAnsi(legendParts.join(theme.fg("dim", " • ")), width)];
|
|
139
175
|
}
|
|
140
176
|
|
|
141
|
-
function renderCategoryBreakdown(analysis: ContextAnalysis, theme: Theme): string[] {
|
|
177
|
+
function renderCategoryBreakdown(analysis: ContextAnalysis, theme: Theme, width: number): string[] {
|
|
142
178
|
const lines: string[] = [];
|
|
143
|
-
lines.push(
|
|
179
|
+
lines.push(sectionHeader("Usage by category", null, theme, width));
|
|
144
180
|
|
|
145
|
-
const {
|
|
146
|
-
const allCategories: Array<{
|
|
147
|
-
key: CategoryKey | "autocompactBuffer" | "freeSpace";
|
|
181
|
+
const rows: Array<{
|
|
148
182
|
label: string;
|
|
149
183
|
color: Parameters<Theme["fg"]>["0"];
|
|
150
184
|
tokens: number;
|
|
151
185
|
}> = [
|
|
152
186
|
...CATEGORY_ORDER.map((key) => ({
|
|
153
|
-
key,
|
|
154
187
|
label: CATEGORY_LABELS[key],
|
|
155
188
|
color: CATEGORY_COLORS[key],
|
|
156
|
-
tokens: categories[key],
|
|
189
|
+
tokens: analysis.categories[key],
|
|
157
190
|
})),
|
|
158
191
|
{
|
|
159
|
-
key: "autocompactBuffer",
|
|
160
192
|
label: "Autocompact buffer",
|
|
161
193
|
color: "warning" as Parameters<Theme["fg"]>["0"],
|
|
162
|
-
tokens: categories.autocompactBuffer,
|
|
194
|
+
tokens: analysis.categories.autocompactBuffer,
|
|
163
195
|
},
|
|
164
196
|
{
|
|
165
|
-
key: "freeSpace",
|
|
166
197
|
label: "Free space",
|
|
167
198
|
color: "dim" as Parameters<Theme["fg"]>["0"],
|
|
168
|
-
tokens: categories.freeSpace,
|
|
199
|
+
tokens: analysis.categories.freeSpace,
|
|
169
200
|
},
|
|
170
201
|
];
|
|
171
202
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
|
|
203
|
+
const labelWidth = Math.max(18, Math.min(22, width - 22));
|
|
204
|
+
for (const row of rows) {
|
|
205
|
+
if (row.tokens <= 0 && row.label !== "Free space") continue;
|
|
206
|
+
const bullet = theme.fg(row.color, "●");
|
|
207
|
+
const label = padRight(row.label, labelWidth);
|
|
208
|
+
const tokens = padLeft(formatTokens(row.tokens), 8);
|
|
209
|
+
const percentage = padLeft(pct(row.tokens, analysis.contextWindow), 7);
|
|
210
|
+
lines.push(truncateToWidth(` ${bullet} ${label} ${tokens} ${percentage}`, width));
|
|
178
211
|
}
|
|
179
212
|
|
|
180
213
|
return lines;
|
|
181
214
|
}
|
|
182
215
|
|
|
183
|
-
function
|
|
184
|
-
|
|
185
|
-
|
|
216
|
+
function renderCompositionGuidelineSubRows(
|
|
217
|
+
sources: Array<{ source: string; tokens: number }>,
|
|
218
|
+
opts: { subLabelWidth: number; total: number; theme: Theme; width: number },
|
|
219
|
+
): string[] {
|
|
220
|
+
const lines: string[] = [];
|
|
221
|
+
for (const item of sources) {
|
|
222
|
+
const label =
|
|
223
|
+
item.source === "default" ? "default" : item.source === "other" ? "extensions" : item.source;
|
|
224
|
+
lines.push(
|
|
225
|
+
truncateToWidth(
|
|
226
|
+
` ${opts.theme.fg("dim", padRight(label, opts.subLabelWidth))} ${padLeft(formatTokens(item.tokens), 8)} ${padLeft(pct(item.tokens, opts.total), 7)}`,
|
|
227
|
+
opts.width,
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
return lines;
|
|
232
|
+
}
|
|
186
233
|
|
|
234
|
+
function renderCompositionSnippetSubRows(
|
|
235
|
+
details: Array<{ name: string; tokens: number }>,
|
|
236
|
+
subLabelWidth: number,
|
|
237
|
+
theme: Theme,
|
|
238
|
+
width: number,
|
|
239
|
+
): string[] {
|
|
187
240
|
const lines: string[] = [];
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
241
|
+
for (const item of details) {
|
|
242
|
+
lines.push(
|
|
243
|
+
truncateToWidth(
|
|
244
|
+
` ${theme.fg("dim", padRight(item.name, subLabelWidth))} ${padLeft(formatTokens(item.tokens), 8)}`,
|
|
245
|
+
width,
|
|
246
|
+
),
|
|
247
|
+
);
|
|
192
248
|
}
|
|
193
249
|
return lines;
|
|
194
250
|
}
|
|
195
251
|
|
|
196
|
-
function
|
|
197
|
-
|
|
198
|
-
|
|
252
|
+
function renderSystemPromptComposition(
|
|
253
|
+
analysis: ContextAnalysis,
|
|
254
|
+
theme: Theme,
|
|
255
|
+
width: number,
|
|
256
|
+
): string[] {
|
|
257
|
+
const breakdown = analysis.systemPromptBreakdown;
|
|
258
|
+
const instructionFileTokens = sum(breakdown.instructionFiles.map((f) => f.tokens));
|
|
259
|
+
const contextFileTokens = sum(breakdown.contextFiles.map((f) => f.tokens));
|
|
260
|
+
const skillTokens = sum(breakdown.skills.map((s) => s.tokens));
|
|
261
|
+
|
|
262
|
+
// Use sum of all breakdown components as the denominator so percentages
|
|
263
|
+
// stay internally consistent even when the system prompt token count has
|
|
264
|
+
// been scaled to actual model usage.
|
|
265
|
+
const total =
|
|
266
|
+
breakdown.base +
|
|
267
|
+
instructionFileTokens +
|
|
268
|
+
contextFileTokens +
|
|
269
|
+
skillTokens +
|
|
270
|
+
breakdown.guidelines +
|
|
271
|
+
breakdown.toolSnippets +
|
|
272
|
+
breakdown.appendText;
|
|
199
273
|
|
|
200
274
|
const lines: string[] = [];
|
|
201
|
-
lines.push(
|
|
202
|
-
|
|
203
|
-
|
|
275
|
+
lines.push(
|
|
276
|
+
sectionHeader(
|
|
277
|
+
"System prompt composition",
|
|
278
|
+
total > 0 ? `${formatTokens(total)} tokens` : null,
|
|
279
|
+
theme,
|
|
280
|
+
width,
|
|
281
|
+
),
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
const labelWidth = Math.max(18, Math.min(22, width - 22));
|
|
285
|
+
const subLabelWidth = labelWidth;
|
|
286
|
+
|
|
287
|
+
const rows = [
|
|
288
|
+
{ label: "Base", color: "accent" as Parameters<Theme["fg"]>["0"], tokens: breakdown.base },
|
|
289
|
+
{
|
|
290
|
+
label: "Instruction files",
|
|
291
|
+
color: "text" as Parameters<Theme["fg"]>["0"],
|
|
292
|
+
tokens: instructionFileTokens,
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
label: "Context files",
|
|
296
|
+
color: "text" as Parameters<Theme["fg"]>["0"],
|
|
297
|
+
tokens: contextFileTokens,
|
|
298
|
+
},
|
|
299
|
+
{ label: "Skills", color: "text" as Parameters<Theme["fg"]>["0"], tokens: skillTokens },
|
|
300
|
+
{
|
|
301
|
+
label: "Guidelines",
|
|
302
|
+
color: "text" as Parameters<Theme["fg"]>["0"],
|
|
303
|
+
tokens: breakdown.guidelines,
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
label: "Tool snippets",
|
|
307
|
+
color: "text" as Parameters<Theme["fg"]>["0"],
|
|
308
|
+
tokens: breakdown.toolSnippets,
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
label: "Append text",
|
|
312
|
+
color: "text" as Parameters<Theme["fg"]>["0"],
|
|
313
|
+
tokens: breakdown.appendText,
|
|
314
|
+
},
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
for (const row of rows) {
|
|
318
|
+
if (row.tokens <= 0) continue;
|
|
319
|
+
const bullet = theme.fg(row.color, "●");
|
|
320
|
+
const label = padRight(row.label, labelWidth);
|
|
321
|
+
const tokens = padLeft(formatTokens(row.tokens), 8);
|
|
322
|
+
const percentage = padLeft(pct(row.tokens, total), 7);
|
|
323
|
+
lines.push(truncateToWidth(` ${bullet} ${label} ${tokens} ${percentage}`, width));
|
|
324
|
+
|
|
325
|
+
if (row.label === "Guidelines" && breakdown.guidelineSources.length > 0) {
|
|
326
|
+
lines.push(
|
|
327
|
+
...renderCompositionGuidelineSubRows(breakdown.guidelineSources, {
|
|
328
|
+
subLabelWidth,
|
|
329
|
+
total,
|
|
330
|
+
theme,
|
|
331
|
+
width,
|
|
332
|
+
}),
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (row.label === "Tool snippets" && breakdown.toolSnippetDetails.length > 0) {
|
|
337
|
+
lines.push(
|
|
338
|
+
...renderCompositionSnippetSubRows(
|
|
339
|
+
breakdown.toolSnippetDetails,
|
|
340
|
+
subLabelWidth,
|
|
341
|
+
theme,
|
|
342
|
+
width,
|
|
343
|
+
),
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return lines;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
interface FileSectionOptions {
|
|
352
|
+
title: string;
|
|
353
|
+
subtitle: string;
|
|
354
|
+
files: Array<{ path: string; tokens: number; lines: number; extra?: string }>;
|
|
355
|
+
total: number;
|
|
356
|
+
theme: Theme;
|
|
357
|
+
width: number;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function renderFileSection(options: FileSectionOptions): string[] {
|
|
361
|
+
const { title, subtitle, files, total, theme, width } = options;
|
|
362
|
+
if (files.length === 0) return [];
|
|
363
|
+
|
|
364
|
+
const sorted = [...files].sort((a, b) => b.tokens - a.tokens);
|
|
365
|
+
const totalTokens = sum(sorted.map((file) => file.tokens));
|
|
366
|
+
const header = sectionHeader(
|
|
367
|
+
title,
|
|
368
|
+
`${pluralize(sorted.length, "file", "files")}, ${formatTokens(totalTokens)} tokens${subtitle ? ` · ${subtitle}` : ""}`,
|
|
369
|
+
theme,
|
|
370
|
+
width,
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
const tokenWidth = 8;
|
|
374
|
+
const lineWidth = 10;
|
|
375
|
+
const pctWidth = 7;
|
|
376
|
+
const extraWidth = sorted.some((file) => file.extra) ? 10 : 0;
|
|
377
|
+
const reserved =
|
|
378
|
+
2 + 2 + lineWidth + 2 + tokenWidth + 2 + pctWidth + (extraWidth ? 2 + extraWidth : 0);
|
|
379
|
+
const pathWidth = Math.max(16, width - reserved);
|
|
380
|
+
|
|
381
|
+
const lines = [header];
|
|
382
|
+
for (const file of sorted) {
|
|
383
|
+
const path = padRight(truncateToWidth(file.path, pathWidth), pathWidth);
|
|
384
|
+
const lineCol = padLeft(`${file.lines} lines`, lineWidth);
|
|
385
|
+
const tokenCol = padLeft(formatTokens(file.tokens), tokenWidth);
|
|
386
|
+
const pctCol = padLeft(pct(file.tokens, total), pctWidth);
|
|
387
|
+
const extra = extraWidth ? ` ${theme.fg("dim", padRight(file.extra ?? "", extraWidth))}` : "";
|
|
204
388
|
lines.push(
|
|
205
|
-
` ${theme.fg("text",
|
|
389
|
+
` ${theme.fg("text", path)} ${theme.fg("dim", lineCol)} ${theme.fg("dim", tokenCol)}${extra} ${theme.fg("dim", pctCol)}`,
|
|
206
390
|
);
|
|
207
391
|
}
|
|
392
|
+
|
|
208
393
|
return lines;
|
|
209
394
|
}
|
|
210
395
|
|
|
211
|
-
function
|
|
396
|
+
function renderInstructionFilesSection(
|
|
397
|
+
analysis: ContextAnalysis,
|
|
398
|
+
theme: Theme,
|
|
399
|
+
width: number,
|
|
400
|
+
): string[] {
|
|
401
|
+
return renderFileSection({
|
|
402
|
+
title: "Instruction Files (AGENTS.md / CLAUDE.md)",
|
|
403
|
+
subtitle: "share of system prompt",
|
|
404
|
+
files: analysis.systemPromptBreakdown.instructionFiles.map((file) => ({
|
|
405
|
+
path: file.path,
|
|
406
|
+
tokens: file.tokens,
|
|
407
|
+
lines: file.lines,
|
|
408
|
+
extra: file.origin,
|
|
409
|
+
})),
|
|
410
|
+
total: analysis.categories.systemPrompt,
|
|
411
|
+
theme,
|
|
412
|
+
width,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function renderContextFilesSection(
|
|
417
|
+
analysis: ContextAnalysis,
|
|
418
|
+
theme: Theme,
|
|
419
|
+
width: number,
|
|
420
|
+
): string[] {
|
|
421
|
+
return renderFileSection({
|
|
422
|
+
title: "Context Files (system prompt)",
|
|
423
|
+
subtitle: "share of system prompt",
|
|
424
|
+
files: analysis.systemPromptBreakdown.contextFiles.map((file) => ({
|
|
425
|
+
path: file.path,
|
|
426
|
+
tokens: file.tokens,
|
|
427
|
+
lines: file.lines,
|
|
428
|
+
})),
|
|
429
|
+
total: analysis.categories.systemPrompt,
|
|
430
|
+
theme,
|
|
431
|
+
width,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function renderInjectedFilesSection(
|
|
436
|
+
analysis: ContextAnalysis,
|
|
437
|
+
theme: Theme,
|
|
438
|
+
width: number,
|
|
439
|
+
): string[] {
|
|
440
|
+
return renderFileSection({
|
|
441
|
+
title: "Context Files (injected · supi-claude-md)",
|
|
442
|
+
subtitle: "share of full context",
|
|
443
|
+
files: analysis.injectedFiles.map((file) => ({
|
|
444
|
+
path: file.file,
|
|
445
|
+
tokens: file.tokens,
|
|
446
|
+
lines: file.lines,
|
|
447
|
+
extra: `turn ${file.turn}`,
|
|
448
|
+
})),
|
|
449
|
+
total: analysis.totalTokens ?? 0,
|
|
450
|
+
theme,
|
|
451
|
+
width,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function renderSkillsSection(analysis: ContextAnalysis, theme: Theme, width: number): string[] {
|
|
212
456
|
const lines: string[] = [];
|
|
213
|
-
|
|
214
|
-
lines.push(
|
|
457
|
+
const total = sum(analysis.skills.map((skill) => skill.tokens));
|
|
458
|
+
lines.push(
|
|
459
|
+
sectionHeader(
|
|
460
|
+
`Skills (${analysis.skills.length})`,
|
|
461
|
+
total > 0 ? `${formatTokens(total)} tokens` : null,
|
|
462
|
+
theme,
|
|
463
|
+
width,
|
|
464
|
+
),
|
|
465
|
+
);
|
|
215
466
|
|
|
216
467
|
if (analysis.skills.length === 0) {
|
|
217
|
-
lines.push(theme.fg("dim", " Send a message to see skill details"));
|
|
468
|
+
lines.push(truncateToWidth(theme.fg("dim", " Send a message to see skill details"), width));
|
|
218
469
|
return lines;
|
|
219
470
|
}
|
|
220
471
|
|
|
221
|
-
|
|
222
|
-
|
|
472
|
+
const skillNameWidth =
|
|
473
|
+
analysis.skills.length > 0 ? Math.max(...analysis.skills.map((s) => s.name.length)) : 0;
|
|
474
|
+
for (const skill of analysis.skills) {
|
|
475
|
+
lines.push(
|
|
476
|
+
truncateToWidth(
|
|
477
|
+
` ${theme.fg("text", padRight(skill.name, skillNameWidth))} ${theme.fg("dim", padLeft(formatTokens(skill.tokens), 8))}`,
|
|
478
|
+
width,
|
|
479
|
+
),
|
|
480
|
+
);
|
|
223
481
|
}
|
|
482
|
+
|
|
224
483
|
return lines;
|
|
225
484
|
}
|
|
226
485
|
|
|
227
|
-
function
|
|
486
|
+
function renderSourceSummaryBar(
|
|
487
|
+
gs: Array<{ source: string; bulletCount: number }>,
|
|
488
|
+
_theme: Theme,
|
|
489
|
+
_width: number,
|
|
490
|
+
): string | null {
|
|
491
|
+
if (gs.length === 0) return null;
|
|
492
|
+
const parts: string[] = [];
|
|
493
|
+
for (const s of gs) {
|
|
494
|
+
const label =
|
|
495
|
+
s.source === "default" ? "default" : s.source === "other" ? "extensions" : s.source;
|
|
496
|
+
parts.push(`${pluralize(s.bulletCount, "bullet", "bullets")} from ${label}`);
|
|
497
|
+
}
|
|
498
|
+
return parts.join(" · ");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function renderBulletLines(
|
|
502
|
+
bullets: string[],
|
|
503
|
+
full: boolean,
|
|
504
|
+
theme: Theme,
|
|
505
|
+
width: number,
|
|
506
|
+
): string[] {
|
|
507
|
+
const previewLimit = full ? bullets.length : Math.min(6, bullets.length);
|
|
508
|
+
const lines: string[] = [];
|
|
509
|
+
for (let i = 0; i < previewLimit; i += 1) {
|
|
510
|
+
const rawText = bullets[i] ?? "";
|
|
511
|
+
const previewText = full || rawText.length <= 90 ? rawText : `${rawText.slice(0, 90)}…`;
|
|
512
|
+
const bullet = `${theme.fg("dim", "•")} ${theme.fg("text", previewText)}`;
|
|
513
|
+
if (full) {
|
|
514
|
+
lines.push(...wrapTextWithAnsi(bullet, Math.max(1, width - 2)).map((line) => ` ${line}`));
|
|
515
|
+
} else {
|
|
516
|
+
lines.push(truncateToWidth(` ${bullet}`, width));
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (!full && bullets.length > previewLimit) {
|
|
520
|
+
lines.push(
|
|
521
|
+
truncateToWidth(
|
|
522
|
+
` ${theme.fg("dim", `… and ${bullets.length - previewLimit} more — run /supi-context full`)}`,
|
|
523
|
+
width,
|
|
524
|
+
),
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
return lines;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function renderGuidelinesSection(analysis: ContextAnalysis, theme: Theme, width: number): string[] {
|
|
531
|
+
const sourceSummary = renderSourceSummaryBar(analysis.guidelineSources, theme, width);
|
|
532
|
+
|
|
533
|
+
const lines = [
|
|
534
|
+
sectionHeader(
|
|
535
|
+
`Guidelines (${pluralize(analysis.guidelineBullets.length, "bullet", "bullets")})`,
|
|
536
|
+
`${formatTokens(analysis.guidelines)} tokens`,
|
|
537
|
+
theme,
|
|
538
|
+
width,
|
|
539
|
+
),
|
|
540
|
+
];
|
|
541
|
+
|
|
542
|
+
if (sourceSummary) {
|
|
543
|
+
lines.push(truncateToWidth(` ${theme.fg("dim", sourceSummary)}`, width));
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const bullets = analysis.guidelineBullets;
|
|
547
|
+
if (bullets.length === 0) {
|
|
548
|
+
return lines;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
lines.push(...renderBulletLines(bullets, analysis.full, theme, width));
|
|
552
|
+
return lines;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function renderToolDefinitionsSection(
|
|
556
|
+
analysis: ContextAnalysis,
|
|
557
|
+
theme: Theme,
|
|
558
|
+
width: number,
|
|
559
|
+
): string[] {
|
|
560
|
+
const tools = [...analysis.toolDefinitions.tools].sort((a, b) => b.tokens - a.tokens);
|
|
561
|
+
if (tools.length === 0) return [];
|
|
562
|
+
|
|
563
|
+
const hasSnippetDetails = analysis.toolSnippetDetails.length > 0;
|
|
564
|
+
|
|
228
565
|
const lines: string[] = [];
|
|
229
|
-
lines.push("");
|
|
230
|
-
lines.push(
|
|
231
|
-
`${theme.fg("text", "Guidelines")} ${theme.fg("dim", formatTokens(analysis.guidelines))}`,
|
|
232
|
-
);
|
|
233
566
|
lines.push(
|
|
234
|
-
|
|
567
|
+
sectionHeader(
|
|
568
|
+
`Tool Definitions (${tools.length} active)`,
|
|
569
|
+
`${formatTokens(analysis.toolDefinitions.tokens)} def tokens${hasSnippetDetails ? ` + ${formatTokens(sum(analysis.toolSnippetDetails.map((s) => s.tokens)))} snippet` : ""}`,
|
|
570
|
+
theme,
|
|
571
|
+
width,
|
|
572
|
+
),
|
|
235
573
|
);
|
|
574
|
+
|
|
575
|
+
const previewLimit = analysis.full ? tools.length : Math.min(5, tools.length);
|
|
576
|
+
const nameWidth = Math.max(12, Math.min(18, Math.max(...tools.map((tool) => tool.name.length))));
|
|
577
|
+
const defTokenWidth = 8;
|
|
578
|
+
const snippetTokenWidth = hasSnippetDetails ? 10 : 0;
|
|
579
|
+
const reserved =
|
|
580
|
+
2 + nameWidth + 2 + defTokenWidth + 2 + (snippetTokenWidth ? snippetTokenWidth + 2 : 0);
|
|
581
|
+
const descWidth = Math.max(12, width - reserved);
|
|
582
|
+
|
|
583
|
+
for (let i = 0; i < previewLimit; i += 1) {
|
|
584
|
+
const tool = tools[i];
|
|
585
|
+
const name = padRight(tool.name, nameWidth);
|
|
586
|
+
const previewDescription =
|
|
587
|
+
analysis.full || tool.description.length <= 50
|
|
588
|
+
? tool.description
|
|
589
|
+
: `${tool.description.slice(0, 50)}…`;
|
|
590
|
+
const description = truncateToWidth(previewDescription, descWidth);
|
|
591
|
+
const defTokens = padLeft(formatTokens(tool.tokens), defTokenWidth);
|
|
592
|
+
const snippet = analysis.toolSnippetDetails.find((s) => s.name === tool.name);
|
|
593
|
+
const snippetCol = snippet
|
|
594
|
+
? ` ${theme.fg("dim", padLeft(`+${formatTokens(snippet.tokens)}snip`, snippetTokenWidth))}`
|
|
595
|
+
: "";
|
|
596
|
+
lines.push(
|
|
597
|
+
truncateToWidth(
|
|
598
|
+
` ${theme.fg("text", name)} ${theme.fg("dim", description)} ${theme.fg("dim", defTokens)}${snippetCol}`,
|
|
599
|
+
width,
|
|
600
|
+
),
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (!analysis.full && tools.length > previewLimit) {
|
|
605
|
+
lines.push(
|
|
606
|
+
truncateToWidth(
|
|
607
|
+
` ${theme.fg("dim", `… and ${tools.length - previewLimit} more — run /supi-context full`)}`,
|
|
608
|
+
width,
|
|
609
|
+
),
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Add a legend row for snippet column if any tools had snippets
|
|
614
|
+
if (hasSnippetDetails) {
|
|
615
|
+
const snippetTotal = sum(analysis.toolSnippetDetails.map((s) => s.tokens));
|
|
616
|
+
lines.push(
|
|
617
|
+
truncateToWidth(
|
|
618
|
+
` ${theme.fg("dim", `→ total tool snippet tokens: ${formatTokens(snippetTotal)}`)}`,
|
|
619
|
+
width,
|
|
620
|
+
),
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
236
624
|
return lines;
|
|
237
625
|
}
|
|
238
626
|
|
|
239
|
-
function renderCompactionNote(analysis: ContextAnalysis, theme: Theme): string[] {
|
|
627
|
+
function renderCompactionNote(analysis: ContextAnalysis, theme: Theme, width: number): string[] {
|
|
240
628
|
if (!analysis.compaction) return [];
|
|
241
629
|
return [
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
630
|
+
truncateToWidth(
|
|
631
|
+
theme.fg(
|
|
632
|
+
"dim",
|
|
633
|
+
`↳ ${pluralize(analysis.compaction.summarizedTurns, "older turn", "older turns")} summarized (compaction)`,
|
|
634
|
+
),
|
|
635
|
+
width,
|
|
246
636
|
),
|
|
247
637
|
];
|
|
248
638
|
}
|
|
249
639
|
|
|
250
|
-
function renderProviderSections(analysis: ContextAnalysis, theme: Theme): string[] {
|
|
640
|
+
function renderProviderSections(analysis: ContextAnalysis, theme: Theme, width: number): string[] {
|
|
251
641
|
if (analysis.providerSections.length === 0) return [];
|
|
252
642
|
|
|
253
643
|
const lines: string[] = [];
|
|
254
644
|
for (const section of analysis.providerSections) {
|
|
255
|
-
lines.push(
|
|
256
|
-
lines.push(theme.fg("accent", section.label));
|
|
645
|
+
lines.push(sectionHeader(section.label, null, theme, width));
|
|
257
646
|
for (const [key, value] of Object.entries(section.data)) {
|
|
258
|
-
|
|
647
|
+
const label = theme.fg("text", key);
|
|
648
|
+
const content = theme.fg("dim", String(value));
|
|
649
|
+
lines.push(truncateToWidth(` ${label}: ${content}`, width));
|
|
259
650
|
}
|
|
260
651
|
}
|
|
652
|
+
|
|
261
653
|
return lines;
|
|
262
654
|
}
|
|
263
655
|
|
|
264
|
-
export function formatContextReport(
|
|
656
|
+
export function formatContextReport(
|
|
657
|
+
analysis: ContextAnalysis,
|
|
658
|
+
theme: Theme,
|
|
659
|
+
width = 200,
|
|
660
|
+
): string[] {
|
|
661
|
+
const safeWidth = Math.max(24, width);
|
|
265
662
|
const lines: string[] = [];
|
|
266
663
|
|
|
267
|
-
lines.push(theme.fg("accent", "◆ Context Usage"));
|
|
664
|
+
lines.push(truncateToWidth(theme.fg("accent", "◆ Context Usage"), safeWidth));
|
|
268
665
|
lines.push("");
|
|
269
|
-
lines.push(...
|
|
666
|
+
lines.push(...renderSummary(analysis, theme, safeWidth));
|
|
270
667
|
lines.push("");
|
|
271
|
-
lines.push(...
|
|
272
|
-
lines.push(
|
|
273
|
-
lines.push(...
|
|
274
|
-
lines.push(
|
|
275
|
-
lines.push(...
|
|
276
|
-
|
|
277
|
-
|
|
668
|
+
lines.push(...renderUsageBar(analysis, theme, safeWidth));
|
|
669
|
+
lines.push("");
|
|
670
|
+
lines.push(...renderCategoryBreakdown(analysis, theme, safeWidth));
|
|
671
|
+
lines.push("");
|
|
672
|
+
lines.push(...renderSystemPromptComposition(analysis, theme, safeWidth));
|
|
673
|
+
|
|
674
|
+
const sections = [
|
|
675
|
+
renderInstructionFilesSection(analysis, theme, safeWidth),
|
|
676
|
+
renderContextFilesSection(analysis, theme, safeWidth),
|
|
677
|
+
renderInjectedFilesSection(analysis, theme, safeWidth),
|
|
678
|
+
renderSkillsSection(analysis, theme, safeWidth),
|
|
679
|
+
renderGuidelinesSection(analysis, theme, safeWidth),
|
|
680
|
+
renderToolDefinitionsSection(analysis, theme, safeWidth),
|
|
681
|
+
renderCompactionNote(analysis, theme, safeWidth),
|
|
682
|
+
renderProviderSections(analysis, theme, safeWidth),
|
|
683
|
+
].filter((section) => section.length > 0);
|
|
684
|
+
|
|
685
|
+
for (const section of sections) {
|
|
686
|
+
lines.push("");
|
|
687
|
+
lines.push(...section);
|
|
688
|
+
}
|
|
278
689
|
|
|
279
690
|
return lines;
|
|
280
691
|
}
|